diff --git a/.github/workflows/auto-close.yml b/.github/workflows/auto-close.yml index dbbb404e9e..aa5d41ff98 100644 --- a/.github/workflows/auto-close.yml +++ b/.github/workflows/auto-close.yml @@ -51,7 +51,7 @@ jobs: run: | gh api graphql \ -f prId="$NODE_ID" \ - -f body="This PR has been automatically closed as the description doesn't follow our template. After you edit it to match the template, the PR will automatically be reopened." \ + -f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). After you edit it to match the template, the PR will automatically be reopened." \ -f query=' mutation CommentAndClosePR($prId: ID!, $body: String!) { addComment(input: { diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 0b10c681a1..c2a3918cfe 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -121,7 +121,7 @@ jobs: cache: true - name: Setup Android SDK - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 with: packages: '' @@ -153,7 +153,7 @@ jobs: fi - name: Publish Android Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-apk-signed path: mobile/build/app/outputs/flutter-apk/*.apk @@ -210,7 +210,7 @@ jobs: working-directory: ./mobile - name: Setup Ruby - uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0 + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 with: ruby-version: '3.3' bundler-cache: true @@ -291,7 +291,7 @@ jobs: security delete-keychain build.keychain || true - name: Upload IPA artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ios-release-ipa path: mobile/ios/Runner.ipa diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index cd05f320c6..3b3eb774cb 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Check for breaking API changes - uses: oasdiff/oasdiff-action/breaking@1f38ea5ea0b4a2e4e49901c3bcdf4386a05e9ea1 # v0.0.37 + uses: oasdiff/oasdiff-action/breaking@e6faebce24cf20ac38653d0d2c7f4aa80aaafc79 # v0.0.38 with: base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 13e71491a6..2a334af89d 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -115,7 +115,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 2055bfce65..0ccebfb363 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -86,7 +86,7 @@ jobs: run: pnpm build - name: Upload build output - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-build-output path: docs/build/ diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 05c845ccd1..8aa063e1bb 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -29,7 +29,7 @@ jobs: run: echo 'The triggering workflow did not succeed' && exit 1 - name: Get artifact id: get-artifact - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.token.outputs.token }} script: | @@ -48,7 +48,7 @@ jobs: return { found: true, id: matchArtifact.id }; - name: Determine deploy parameters id: parameters - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: HEAD_SHA: ${{ github.event.workflow_run.head_sha }} with: @@ -135,7 +135,7 @@ jobs: - name: Load parameters id: parameters - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: PARAM_JSON: ${{ needs.checks.outputs.parameters }} with: @@ -147,7 +147,7 @@ jobs: core.setOutput("shouldDeploy", parameters.shouldDeploy); - name: Download artifact - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }} with: diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index ae8e0b29ca..59cbb28fa8 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -29,7 +29,7 @@ jobs: persist-credentials: true - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -42,13 +42,13 @@ jobs: run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix - name: Commit and push - uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 + uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0 with: default_author: github_actions message: 'chore: fix formatting' - name: Remove label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 if: always() with: github-token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/merge-translations.yml b/.github/workflows/merge-translations.yml index fcda857eda..08d3192f8b 100644 --- a/.github/workflows/merge-translations.yml +++ b/.github/workflows/merge-translations.yml @@ -31,7 +31,7 @@ jobs: - name: Generate a token id: generate_token if: ${{ inputs.skip != true }} - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index dec9b06d67..5731c06372 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -63,10 +63,10 @@ jobs: ref: main - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0 - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -86,7 +86,7 @@ jobs: - name: Commit and tag id: push-tag - uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 + uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0 with: default_author: github_actions message: 'chore: version ${{ steps.output.outputs.version }}' @@ -124,7 +124,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml index c63ef17f20..5cf0008597 100644 --- a/.github/workflows/preview-label.yaml +++ b/.github/workflows/preview-label.yaml @@ -37,7 +37,7 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.token.outputs.token }} script: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a606aba124..4558b90866 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -464,7 +464,7 @@ jobs: run: docker compose logs --no-color > docker-compose-logs.txt working-directory: ./e2e - name: Archive Docker logs - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: e2e-server-docker-logs-${{ matrix.runner }} @@ -522,7 +522,7 @@ jobs: run: pnpm test:web if: ${{ !cancelled() }} - name: Archive e2e test (web) results - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: success() || failure() with: name: e2e-web-test-results-${{ matrix.runner }} @@ -533,7 +533,7 @@ jobs: run: pnpm test:web:ui if: ${{ !cancelled() }} - name: Archive ui test (web) results - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: success() || failure() with: name: e2e-ui-test-results-${{ matrix.runner }} @@ -544,7 +544,7 @@ jobs: run: pnpm test:web:maintenance if: ${{ !cancelled() }} - name: Archive maintenance tests (web) results - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: success() || failure() with: name: e2e-maintenance-isolated-test-results-${{ matrix.runner }} @@ -554,7 +554,7 @@ jobs: run: docker compose logs --no-color > docker-compose-logs.txt working-directory: ./e2e - name: Archive Docker logs - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: e2e-web-docker-logs-${{ matrix.runner }} @@ -620,7 +620,7 @@ jobs: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: python-version: 3.11 - name: Install dependencies diff --git a/cli/package.json b/cli/package.json index b195a1418d..108e65f945 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.7.4", + "version": "2.7.5", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -20,7 +20,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^24.12.0", + "@types/node": "^24.12.2", "@vitest/coverage-v8": "^4.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 21700ef963..f179b350c9 100644 --- a/cli/src/commands/asset.spec.ts +++ b/cli/src/commands/asset.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; import { describe, expect, it, MockedFunction, vi } from 'vitest'; -import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; +import { AssetRejectReason, AssetUploadAction, checkBulkUpload, defaults, getSupportedMediaTypes } from '@immich/sdk'; import createFetchMock from 'vitest-fetch-mock'; import { @@ -120,7 +120,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -144,10 +144,10 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Reject, + action: AssetUploadAction.Reject, id: testFilePath, assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5', - reason: Reason.Duplicate, + reason: AssetRejectReason.Duplicate, }, ], }); @@ -167,7 +167,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -187,7 +187,7 @@ describe('checkForDuplicates', () => { mocked.mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index 7d4b09b69d..2c6430c83a 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -1,9 +1,9 @@ import { - Action, AssetBulkUploadCheckItem, AssetBulkUploadCheckResult, AssetMediaResponseDto, AssetMediaStatus, + AssetUploadAction, Permission, addAssetsToAlbum, checkBulkUpload, @@ -234,7 +234,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas const results = response.results as AssetBulkUploadCheckResults; for (const { id: filepath, assetId, action } of results) { - if (action === Action.Accept) { + if (action === AssetUploadAction.Accept) { newFiles.push(filepath); } else { // rejects are always duplicates @@ -404,8 +404,6 @@ const uploadFile = async (input: string, stats: Stats): Promise Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename. +Date and time variables in storage templates are rendered in the server's local timezone. + ```bash title="Default template" Year/Year-Month-Day/Filename.Extension ``` diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 624e95c65b..964291ad08 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,7 +1,7 @@ [ { - "label": "v2.7.4", - "url": "https://docs.v2.7.4.archive.immich.app" + "label": "v2.7.5", + "url": "https://docs.v2.7.5.archive.immich.app" }, { "label": "v2.6.3", diff --git a/e2e-auth-server/auth-server.ts b/e2e-auth-server/auth-server.ts index 9aef56510d..bcfeca1e1c 100644 --- a/e2e-auth-server/auth-server.ts +++ b/e2e-auth-server/auth-server.ts @@ -66,7 +66,9 @@ const getClaims = (sub: string, use?: string) => { }; const setup = async () => { - const { privateKey, publicKey } = await generateKeyPair('RS256'); + const { privateKey, publicKey } = await generateKeyPair('RS256', { + extractable: true, + }); const redirectUris = [ 'http://127.0.0.1:2285/auth/login', diff --git a/e2e-auth-server/package.json b/e2e-auth-server/package.json index 73ede1b7c4..f8ea7243fd 100644 --- a/e2e-auth-server/package.json +++ b/e2e-auth-server/package.json @@ -7,7 +7,7 @@ "start": "tsx startup.ts" }, "devDependencies": { - "jose": "^5.6.3", + "jose": "^6.0.0", "@types/oidc-provider": "^9.0.0", "oidc-provider": "^9.0.0", "tsx": "^4.20.6" diff --git a/e2e/package.json b/e2e/package.json index bd83e00ed8..6b72c1b36d 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "2.7.4", + "version": "2.7.5", "description": "", "main": "index.js", "type": "module", @@ -32,7 +32,7 @@ "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^24.12.0", + "@types/node": "^24.12.2", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^7.0.0", diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts index a9e90940ab..3725de8d26 100644 --- a/e2e/src/specs/server/api/album.e2e-spec.ts +++ b/e2e/src/specs/server/api/album.e2e-spec.ts @@ -130,12 +130,11 @@ describe('/albums', () => { describe('GET /albums', () => { it("should not show other users' favorites", async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toEqual(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ isFavorite: false })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -304,13 +303,12 @@ describe('/albums', () => { describe('GET /albums/:id', () => { it('should return album info for own album', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -322,7 +320,7 @@ describe('/albums', () => { it('should return album info for shared album (editor)', async () => { const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -331,14 +329,14 @@ describe('/albums', () => { it('should return album info for shared album (viewer)', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[3].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[3].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toBe(200); expect(body).toMatchObject({ id: user1Albums[3].id }); }); - it('should return album info with assets when withoutAssets is undefined', async () => { + it('should return album info', async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); @@ -346,25 +344,6 @@ describe('/albums', () => { expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], - contributorCounts: [{ userId: user1.userId, assetCount: 1 }], - lastModifiedAssetTimestamp: expect.any(String), - startDate: expect.any(String), - endDate: expect.any(String), - albumUsers: expect.any(Array), - shared: true, - }); - }); - - it('should return album info without assets when withoutAssets is true', async () => { - const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=true`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...user1Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -379,13 +358,12 @@ describe('/albums', () => { await utils.deleteAssets(user1.accessToken, [user1Asset2.id]); const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=true`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user2Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -426,7 +404,6 @@ describe('/albums', () => { shared: false, albumUsers: [], hasSharedLink: false, - assets: [], assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), isActivityEnabled: true, diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts index 11e825a7cd..3fbacd5bf6 100644 --- a/e2e/src/specs/server/api/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -1,7 +1,6 @@ import { AssetMediaResponseDto, AssetMediaStatus, - AssetResponseDto, AssetTypeEnum, AssetVisibility, getAssetInfo, @@ -19,7 +18,7 @@ import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -95,8 +94,8 @@ describe('/asset', () => { utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken, { isFavorite: true, - fileCreatedAt: yesterday.toISO(), - fileModifiedAt: yesterday.toISO(), + fileCreatedAt: yesterday.toUTC().toISO(), + fileModifiedAt: yesterday.toUTC().toISO(), assetData: { filename: 'example.mp4' }, }), utils.createAsset(user1.accessToken), @@ -380,62 +379,12 @@ describe('/asset', () => { }); }); - describe('GET /assets/random', () => { - beforeAll(async () => { - await Promise.all([ - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - utils.createAsset(user1.accessToken), - ]); - - await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); - }); - - it.each(TEN_TIMES)('should return 1 random assets', async () => { - const { status, body } = await request(app) - .get('/assets/random') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(1); - expect(assets[0].ownerId).toBe(user1.userId); - }); - - it.each(TEN_TIMES)('should return 2 random assets', async () => { - const { status, body } = await request(app) - .get('/assets/random?count=2') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(2); - - for (const asset of assets) { - expect(asset.ownerId).toBe(user1.userId); - } - }); - - it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => { - const { status, body } = await request(app) - .get('/assets/random') - .set('Authorization', `Bearer ${user2.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); - }); - }); - describe('PUT /assets/:id', () => { it('should require access', async () => { const { status, body } = await request(app) .put(`/assets/${user2Assets[0].id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({}); expect(status).toBe(400); expect(body).toEqual(errorDto.noPermission); }); @@ -1142,8 +1091,6 @@ describe('/asset', () => { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', makeRandomImage(), 'example.jpg'); @@ -1160,8 +1107,6 @@ describe('/asset', () => { const { body, status } = await request(app) .post('/assets') .set('Authorization', `Bearer ${quotaUser.accessToken}`) - .field('deviceAssetId', 'example-image') - .field('deviceId', 'e2e') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .attach('assetData', randomBytes(2014), 'example.jpg'); @@ -1215,29 +1160,4 @@ describe('/asset', () => { expect(video.checksum).toStrictEqual(checksum); }); }); - - describe('POST /assets/exist', () => { - it('ignores invalid deviceAssetIds', async () => { - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['invalid', 'INVALID'], - }); - - expect(response.existingIds).toHaveLength(0); - }); - - it('returns the IDs of existing assets', async () => { - await utils.createAsset(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetId: 'test-asset-0', - }); - - const response = await utils.checkExistingAssets(user1.accessToken, { - deviceId: 'test-assets-exist', - deviceAssetIds: ['test-asset-0'], - }); - - expect(response.existingIds).toEqual(['test-asset-0']); - }); - }); }); diff --git a/e2e/src/specs/server/api/library.e2e-spec.ts b/e2e/src/specs/server/api/library.e2e-spec.ts index 4d67a84647..719436a66d 100644 --- a/e2e/src/specs/server/api/library.e2e-spec.ts +++ b/e2e/src/specs/server/api/library.e2e-spec.ts @@ -110,7 +110,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should not create an external library with duplicate exclusion patterns', async () => { @@ -125,7 +125,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); }); @@ -157,7 +157,7 @@ describe('/libraries', () => { .send({ name: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters'])); }); it('should change the import paths', async () => { @@ -181,7 +181,7 @@ describe('/libraries', () => { .send({ importPaths: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty'])); }); it('should reject duplicate import paths', async () => { @@ -191,7 +191,7 @@ describe('/libraries', () => { .send({ importPaths: ['/path', '/path'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should change the exclusion pattern', async () => { @@ -215,7 +215,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); it('should reject an empty exclusion pattern', async () => { @@ -225,7 +225,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty'])); }); }); diff --git a/e2e/src/specs/server/api/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts index 977638aa24..c280deb134 100644 --- a/e2e/src/specs/server/api/map.e2e-spec.ts +++ b/e2e/src/specs/server/api/map.e2e-spec.ts @@ -109,7 +109,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lon=123') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is not a number', async () => { @@ -117,7 +117,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=abc&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is out of range', async () => { @@ -125,7 +125,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=91&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90'])); }); it('should throw an error if a lon is not provided', async () => { @@ -133,7 +133,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=75') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180'])); + expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN'])); }); const reverseGeocodeTestCases = [ diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index ae9064375f..98cb28c821 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -76,6 +76,7 @@ const setupOAuth = async (token: string, dto: Partial) => ...defaults.oauth, buttonText: 'Login with Immich', issuerUrl: `${authServer.internal}/.well-known/openid-configuration`, + allowInsecureRequests: true, ...dto, }; await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options); @@ -101,7 +102,7 @@ describe(`/oauth`, () => { it(`should throw an error if a redirect uri is not provided`, async () => { const { status, body } = await request(app).post('/oauth/authorize').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined'])); }); it('should return a redirect uri', async () => { @@ -123,13 +124,13 @@ describe(`/oauth`, () => { it(`should throw an error if a url is not provided`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url must be a string', 'url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined'])); }); it(`should throw an error if the url is empty`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({ url: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters'])); }); it(`should throw an error if the state is not provided`, async () => { @@ -258,7 +259,7 @@ describe(`/oauth`, () => { accessToken: expect.any(String), isAdmin: false, name: 'OAuth User', - userEmail: 'oauth-RS256-token@immich.app', + userEmail: 'oauth-rs256-token@immich.app', userId: expect.any(String), }); }); @@ -399,4 +400,23 @@ describe(`/oauth`, () => { }); }); }); + + describe('allowInsecureRequests: false', () => { + beforeAll(async () => { + await setupOAuth(admin.accessToken, { + enabled: true, + clientId: OAuthClient.DEFAULT, + clientSecret: OAuthClient.DEFAULT, + allowInsecureRequests: false, + }); + }); + + it('should reject OAuth discovery over HTTP', async () => { + const { status, body } = await request(app) + .post('/oauth/authorize') + .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' }); + expect(status).toBe(500); + expect(body).toMatchObject({ statusCode: 500 }); + }); + }); }); diff --git a/e2e/src/specs/server/api/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts index 2f6ea75f77..e3e17f67c2 100644 --- a/e2e/src/specs/server/api/search.e2e-spec.ts +++ b/e2e/src/specs/server/api/search.e2e-spec.ts @@ -74,7 +74,6 @@ describe('/search', () => { const bytes = await readFile(join(testAssetDir, filename)); assets.push( await utils.createAsset(admin.accessToken, { - deviceAssetId: `test-${filename}`, assetData: { bytes, filename }, ...dto, }), @@ -458,7 +457,7 @@ describe('/search', () => { expect(Array.isArray(body)).toBe(true); if (Array.isArray(body)) { expect(body.length).toBeGreaterThan(10); - expect(body[0].name).toEqual(name); + expect(body[0].name).toEqual(expect.stringContaining(name)); expect(body[0].admin2name).toEqual(name); } }); diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index 3dd6f15e71..1220e6cab5 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -207,16 +207,6 @@ describe('/server', () => { }); }); - describe('GET /server/theme', () => { - it('should respond with the server theme', async () => { - const { status, body } = await request(app).get('/server/theme'); - expect(status).toBe(200); - expect(body).toEqual({ - customCss: '', - }); - }); - }); - describe('GET /server/license', () => { it('should require authentication', async () => { const { status, body } = await request(app).get('/server/license'); diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts index 00c455d6cb..1d069d0f54 100644 --- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -243,9 +243,21 @@ describe('/shared-links', () => { }); it('should get data for correct password protected link', async () => { + const response = await request(app) + .post('/shared-links/login') + .send({ password: 'foo' }) + .query({ key: linkWithPassword.key }); + + expect(response.status).toBe(201); + + const cookies = response.get('Set-Cookie') ?? []; + expect(cookies).toHaveLength(1); + expect(cookies[0]).toContain('immich_shared_link_token'); + const { status, body } = await request(app) .get('/shared-links/me') - .query({ key: linkWithPassword.key, password: 'foo' }); + .query({ key: linkWithPassword.key }) + .set('Cookie', cookies); expect(status).toBe(200); expect(body).toEqual( diff --git a/e2e/src/specs/server/api/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts index d69536f3a3..7b5a2f16de 100644 --- a/e2e/src/specs/server/api/tag.e2e-spec.ts +++ b/e2e/src/specs/server/api/tag.e2e-spec.ts @@ -309,7 +309,7 @@ describe('/tags', () => { .get(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should get tag details', async () => { @@ -427,7 +427,7 @@ describe('/tags', () => { .delete(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should delete a tag', async () => { diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts index 793c508a36..6751b21e84 100644 --- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts +++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts @@ -287,7 +287,8 @@ describe('/admin/users', () => { it('should delete user', async () => { const { status, body } = await request(app) .delete(`/admin/users/${userToDelete.userId}`) - .set('Authorization', `Bearer ${admin.accessToken}`); + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({}); expect(status).toBe(200); expect(body).toMatchObject({ diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 3f280dddf5..ee13a29c1b 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -178,7 +178,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number'])); + expect(body).toEqual( + errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']), + ); }); it('should update download archive size', async () => { @@ -204,7 +206,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']), + ); }); it('should update download include embedded videos', async () => { diff --git a/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts index 2f90e4e3d8..bbe0ef328f 100644 --- a/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts +++ b/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts @@ -1,7 +1,9 @@ import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk'; import { expect, test } from '@playwright/test'; +import { readFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; import type { Socket } from 'socket.io-client'; -import { utils } from 'src/utils'; +import { testAssetDir, utils } from 'src/utils'; test.describe('Detail Panel', () => { let admin: LoginResponseDto; @@ -83,4 +85,42 @@ test.describe('Detail Panel', () => { await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); await expect(textarea).toHaveValue('new description'); }); + + test.describe('Date editor', () => { + test('displays inferred asset timezone', async ({ context, page }) => { + const test = { + filepath: 'metadata/dates/datetimeoriginal-gps.jpg', + expected: { + dateTime: '2025-12-01T11:30', + // Test with a timezone which is NOT the first among timezones with the same offset + // This is to check that the editor does not simply fall back to the first available timezone with that offset + // America/Denver (-07:00) is not the first among timezones with offset -07:00 + timeZoneWithOffset: 'America/Denver (-07:00)', + }, + }; + + const asset = await utils.createAsset(admin.accessToken, { + assetData: { + bytes: await readFile(join(testAssetDir, test.filepath)), + filename: basename(test.filepath), + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); + + // asset viewer -> detail panel -> date editor + await utils.setAuthCookies(context, admin.accessToken); + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + + await page.getByRole('button', { name: 'Info' }).click(); + await page.getByTestId('detail-panel-edit-date-button').click(); + await page.waitForSelector('[role="dialog"]'); + + const datetime = page.locator('#datetime'); + await expect(datetime).toHaveValue(test.expected.dateTime); + const timezone = page.getByRole('combobox', { name: 'Timezone' }); + await expect(timezone).toHaveValue(test.expected.timeZoneWithOffset); + }); + }); }); diff --git a/e2e/src/specs/web/duplicates.e2e-spec.ts b/e2e/src/specs/web/duplicates.e2e-spec.ts index 34f11cdf78..c39e9019d3 100644 --- a/e2e/src/specs/web/duplicates.e2e-spec.ts +++ b/e2e/src/specs/web/duplicates.e2e-spec.ts @@ -16,8 +16,8 @@ test.describe('Duplicates Utility', () => { test.beforeEach(async ({ context }) => { [firstAsset, secondAsset] = await Promise.all([ - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-a' }), - utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-b' }), + utils.createAsset(admin.accessToken, {}), + utils.createAsset(admin.accessToken, {}), ]); await updateAssets( diff --git a/e2e/src/specs/web/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts index 76d9d61ed6..71f2145be8 100644 --- a/e2e/src/specs/web/photo-viewer.e2e-spec.ts +++ b/e2e/src/specs/web/photo-viewer.e2e-spec.ts @@ -77,18 +77,4 @@ test.describe('Photo Viewer', () => { }); expect(tagAtCenter).toBe('IMG'); }); - - test('reloads photo when checksum changes', async ({ page }) => { - await page.goto(`/photos/${asset.id}`); - - const preview = page.getByTestId('preview').filter({ visible: true }); - await expect(preview).toHaveAttribute('src', /.+/); - const initialSrc = await preview.getAttribute('src'); - - const websocketEvent = utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); - await utils.replaceAsset(admin.accessToken, asset.id); - await websocketEvent; - - await expect(preview).not.toHaveAttribute('src', initialSrc!); - }); }); diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index 0c4bd06dc3..8fc9ce331d 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -315,11 +315,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons return { id: asset.id, - deviceAssetId: `device-${asset.id}`, ownerId: asset.ownerId, owner: owner || defaultOwner, libraryId: `library-${asset.ownerId}`, - deviceId: `device-${asset.ownerId}`, type: asset.isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image, originalPath: `/original/${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, originalFileName: `${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`, @@ -334,7 +332,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons isArchived: false, isTrashed: asset.isTrashed, visibility: asset.visibility, - duration: asset.duration || '0:00:00.00000', + duration: asset.duration, exifInfo, livePhotoVideoId: asset.livePhotoVideoId, tags: [], @@ -429,7 +427,6 @@ export function getAlbum( hasSharedLink: false, isActivityEnabled: true, assetCount: albumAssets.length, - assets: albumAssets, startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined, endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, diff --git a/e2e/src/ui/mock-network/broken-asset-network.ts b/e2e/src/ui/mock-network/broken-asset-network.ts index 1494b40531..ce66412e61 100644 --- a/e2e/src/ui/mock-network/broken-asset-network.ts +++ b/e2e/src/ui/mock-network/broken-asset-network.ts @@ -16,7 +16,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { const now = new Date().toISOString(); return { id: assetId, - deviceAssetId: `device-${assetId}`, ownerId, owner: { id: ownerId, @@ -27,7 +26,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { avatarColor: 'blue' as never, }, libraryId: `library-${ownerId}`, - deviceId: `device-${ownerId}`, type: AssetTypeEnum.Image, originalPath: `/original/${assetId}.jpg`, originalFileName: `${assetId}.jpg`, @@ -42,7 +40,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { isArchived: false, isTrashed: false, visibility: AssetVisibility.Timeline, - duration: '0:00:00.00000', + duration: null, exifInfo: { make: null, model: null, @@ -69,7 +67,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { tags: [], people: [], unassignedFaces: [], - stack: null, + stack: undefined, isOffline: false, hasMetadata: true, duplicateId: null, diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 4d44d99e2f..aa4c3b8499 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -3,7 +3,6 @@ import { AssetMediaResponseDto, AssetResponseDto, AssetVisibility, - CheckExistingAssetsDto, CreateAlbumDto, CreateLibraryDto, JobCreateDto, @@ -20,7 +19,6 @@ import { UserAdminCreateDto, UserPreferencesUpdateDto, ValidateLibraryDto, - checkExistingAssets, createAlbum, createApiKey, createJob, @@ -343,8 +341,6 @@ export const utils = { }, ) => { const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), ...dto, @@ -375,40 +371,6 @@ export const utils = { return body as AssetMediaResponseDto; }, - replaceAsset: async ( - accessToken: string, - assetId: string, - dto?: Partial> & { assetData?: FileData }, - ) => { - const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', - fileCreatedAt: new Date().toISOString(), - fileModifiedAt: new Date().toISOString(), - ...dto, - }; - - const assetData = dto?.assetData?.bytes || makeRandomImage(); - const filename = dto?.assetData?.filename || 'example.png'; - - if (dto?.assetData?.bytes) { - console.log(`Uploading ${filename}`); - } - - const builder = request(app) - .put(`/assets/${assetId}/original`) - .attach('assetData', assetData, filename) - .set('Authorization', `Bearer ${accessToken}`); - - for (const [key, value] of Object.entries(_dto)) { - void builder.field(key, String(value)); - } - - const { body } = await builder; - - return body as AssetMediaResponseDto; - }, - createImageFile: (path: string) => { if (!existsSync(dirname(path))) { mkdirSync(dirname(path), { recursive: true }); @@ -450,9 +412,6 @@ export const utils = { getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), - checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) => - checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }), - searchAssets: async (accessToken: string, dto: MetadataSearchDto) => { return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); }, diff --git a/e2e/test-assets b/e2e/test-assets index 163c251744..0eac5a3738 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 163c251744e0a35d7ecfd02682452043f149fc2b +Subproject commit 0eac5a37384c151be88381b41f9e28d8d59a4466 diff --git a/i18n/ar.json b/i18n/ar.json index c6899c61fc..fe0b9d072c 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -3,7 +3,7 @@ "account": "حساب", "account_settings": "إعدادات الحساب", "acknowledge": "أُدرك ذلك", - "action": "عملية", + "action": "إجراء", "action_common_update": "تحديث", "action_description": "مجموعة من الفعاليات التي ستنفذ على الأصول التي تم تصفيتها", "actions": "عمليات", @@ -61,8 +61,8 @@ "backup_onboarding_1_description": "نسخة خارج الموقع في موقع آخر.", "backup_onboarding_2_description": "نسخ محلية على أجهزة مختلفة. يشمل ذلك الملفات الرئيسية ونسخة احتياطية محلية منها.", "backup_onboarding_3_description": "إجمالي نُسخ بياناتك، بما في ذلك الملفات الأصلية. يشمل ذلك نسخةً واحدةً خارج الموقع ونسختين محليتين.", - "backup_onboarding_description": "يُنصح باتباع استراتيجية النسخ الاحتياطي 3-2-1 لحماية بياناتك. احتفظ بنسخ احتياطية من صورك/فيديوهاتك المحمّلة، بالإضافة إلى قاعدة بيانات Immich، لضمان حل نسخ احتياطي شامل.", - "backup_onboarding_footer": "لمزيد من المعلومات حول النسخ الاحتياطي لـ Immich، يرجى الرجوع إلى التعليمات .", + "backup_onboarding_description": "يُنصح باتباع استراتيجية النسخ الاحتياطي 3-2- 1 لحماية بياناتك. احتفظ بنسخ احتياطية من صورك/فيديوهاتك المحمّلة، بالإضافة إلى قاعدة بيانات Immich، لضمان حل نسخ احتياطي شامل.", + "backup_onboarding_footer": "لمزيد من المعلومات حول النسخ الاحتياطي لـ Immich، يرجى الرجوع إلى الوثائق.", "backup_onboarding_parts_title": "يتضمن النسخ الاحتياطي 3-2-1 ما يلي:", "backup_onboarding_title": "النسخ الاحتياطية", "backup_settings": "إعدادات تفريغ قاعدة البيانات", @@ -333,7 +333,7 @@ "storage_template_migration_description": "قم بتطبيق القالب الحالي {template} على المحتويات التي تم رفعها سابقًا", "storage_template_migration_info": "تغييرات النموذج الخزني ستغير جميع الصيغ الى احرف صغيرة. تغييرات النموذج ستنطبق فقط على المحتويات الجديدة. لتطبيق النموذج على المحتويات التي تم رفعها سابقًا، قم بتشغيل {job}.", "storage_template_migration_job": "وظيفة تهجير قالب التخزين", - "storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى Storage Template وimplications", + "storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى Storage Template و implications.", "storage_template_onboarding_description_v2": "عند التفعيل. هذه الخاصية ستقوم بالترتيب التلقائي للملفات بناء على نموذج معرف من قبل المستخدم. رجاء اطلع على التوثيق.", "storage_template_path_length": "الحد التقريبي لطول المسار: {length, number}/{limit, number}", "storage_template_settings": "قالب التخزين", @@ -372,7 +372,7 @@ "transcoding_audio_codec": "كود الصوت", "transcoding_audio_codec_description": "Opus هو الخيار ذو أعلى جودة، ولكنه يتمتع بتوافق أقل مع الأجهزة أو البرمجيات القديمة.", "transcoding_bitrate_description": "مقاطع الفيديو التي يتجاوز معدل البت أقصى قيمة أو التي لا تكون في تنسيق مقبول", - "transcoding_codecs_learn_more": "لمعرفة المزيد حول المصطلحات المستخدمة هنا، يرجى الرجوع إلى وثائق FFmpeg للH.264 codec, HEVC codec and VP9 codec.", + "transcoding_codecs_learn_more": "لمعرفة المزيد حول المصطلحات المستخدمة هنا، يرجى الرجوع إلى وثائق FFmpeg لـ H.264 codec، و HEVC codec و VP9 codec.", "transcoding_constant_quality_mode": "وضع الجودة الثابتة", "transcoding_constant_quality_mode_description": "ICQ أفضل من CQP، ولكن بعض أجهزة عتاد التسريع لا تدعم هذا الوضع. تعيين هذا الخيار يسجعل الأفضلية للوضع المحدد عند استخدام الترميز بناءً على الجودة. يتم تجاهله بواسطة NVENC لأنه لا يدعم ICQ.", "transcoding_constant_rate_factor": "عامل معدل الجودة الثابت (-crf)", @@ -2392,7 +2392,7 @@ "view_name": "عرض", "view_next_asset": "عرض المحتوى التالي", "view_previous_asset": "عرض المحتوى السابق", - "view_qr_code": "­عرض رمز الاستجابة السريعة", + "view_qr_code": "عرض رمز الاستجابة السريعة", "view_similar_photos": "عرض صور مشابهة", "view_stack": "عرض التكديس", "view_user": "عرض المستخدم", @@ -2411,14 +2411,14 @@ "welcome_to_immich": "مرحباً بك في Immich", "width": "عُرض", "wifi_name": "اسم شبكة Wi-Fi", - "workflow_delete_prompt": "هل أنت متأكد من حذف سير العمل هذا؟", + "workflow_delete_prompt": "متأكد من حذف سير العمل هذا؟", "workflow_deleted": "تم حذف سير العمل", "workflow_description": "وصف سير العمل", "workflow_info": "معلومات سير العمل", "workflow_json": "ملف JSON لسير العمل", "workflow_json_help": "قم بتعديل إعدادات سير العمل بصيغة JSON. ستتم مزامنة التغييرات مع أداة الإنشاء المرئية.", "workflow_name": "اسم سير العمل", - "workflow_navigation_prompt": "هل انت متاكد من المغادرة بدون حفظ التغييرات؟", + "workflow_navigation_prompt": "متاكد من المغادرة بدون حفظ التغييرات؟", "workflow_summary": "ملخص سير العمل", "workflow_update_success": "تم تحديث سير العمل بنجاح", "workflow_updated": "تم تحديث سير العمل", diff --git a/i18n/da.json b/i18n/da.json index ba186826fd..7628be0f4c 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -849,9 +849,12 @@ "create_link_to_share": "Opret link for at dele", "create_link_to_share_description": "Tillad alle med linket at se de(t) valgte billede(r)", "create_new": "OPRET NY", + "create_new_face": "Opret nyt ansigt", "create_new_person": "Opret ny person", "create_new_person_hint": "Tildel valgte aktiver til en ny person", "create_new_user": "Opret ny bruger", + "create_person": "Opret person", + "create_person_subtitle": "Tilføj et navn til det valgte ansigt for at oprette og tagge den nye person", "create_shared_album_page_share_add_assets": "TILFØJ ELEMENT", "create_shared_album_page_share_select_photos": "Vælg Billeder", "create_shared_link": "Opret delt link", @@ -892,6 +895,7 @@ "day": "Dag", "days": "Dage", "deduplicate_all": "Dedubliker alle", + "default_locale": "Standard sprog", "default_locale_description": "Formatér datoer og tal baseret på din browsers landestandard", "delete": "Slet", "delete_action_confirmation_message": "Er du sikker på, at du vil slette dette objekt? Denne handling vil flytte objektet til serverens papirkurv, og vil spørge dig, om du vil slette den lokalt", @@ -2213,6 +2217,7 @@ "tag": "Tag", "tag_assets": "Tag mediefiler", "tag_created": "Oprettet tag: {tag}", + "tag_face": "Tag ansigt", "tag_feature_description": "Gennemse billeder og videoer grupperet efter logiske tag-emner", "tag_not_found_question": "Kan du ikke finde et tag? Opret et nyt tag.", "tag_people": "Tag personer", diff --git a/i18n/de_CH.json b/i18n/de_CH.json index 5d25d2a142..f4fd0c59de 100644 --- a/i18n/de_CH.json +++ b/i18n/de_CH.json @@ -1,132 +1,132 @@ { "about": "Über", "account": "Konto", - "account_settings": "Konto Istelligä", - "acknowledge": "Bestätige", + "account_settings": "Konto Einstellungen", + "acknowledge": "Bestätigä", "action": "Aktion", "action_common_update": "Update", - "action_description": "Es paar Aktione, wo a de gfilterete Assets usgführt wärde sölled", - "actions": "Aktione", + "action_description": "Aktionä, wo uf de gefilterti Mediä ausgführt werdä solled", + "actions": "Aktionen", "active": "Aktiv", - "active_count": "Aktivi: {count}", + "active_count": "Aktiv: {count}", "activity": "Aktivität", - "activity_changed": "Aktivität isch {enabled, select, true {aktiviert} other {deaktiviert}}", - "add": "Hinzuefüegä", - "add_a_description": "Beschriibig hinzuefüege", - "add_a_location": "Standort hinzuefüege", - "add_a_name": "Name hinzuefüege", - "add_a_title": "Titel hinzuefüege", - "add_action": "Aktion hinzuefüege", - "add_action_description": "Aklicke um en Aktion dure zfüehre", - "add_assets": "Assets hinzufüege", - "add_birthday": "Geburtstag hinzuefüege", + "activity_changed": "Aktivität ist {enabled, select, true {aktiviert} other {deaktiviert}}", + "add": "Hinzuefüge", + "add_a_description": "Beschreibung hinzufügen", + "add_a_location": "Standort hinzuefügä", + "add_a_name": "Namä hinzefügä", + "add_a_title": "Titel hinzufeügä", + "add_action": "Aktion hinzuefügä", + "add_action_description": "Klick do zum e Aktion hinzuefüge", + "add_assets": "Mediä hinzuefüge", + "add_birthday": "Geburtstag hinzuefüge", "add_endpoint": "Endpunkt hinzuefüge", - "add_exclusion_pattern": "Uuschlussmuster hinzuefüege", - "add_filter": "Filter hinzuefüge", - "add_filter_description": "Klicke, um e Filterbedingig hinzuezfüege", - "add_location": "Standort hinzuefüege", - "add_more_users": "Meh Benutzer hinzuefüege", - "add_partner": "Partner hinzuefüege", - "add_path": "Pfad hinzuefüege", - "add_photos": "Föteli hinzuefüege", - "add_tag": "Tag hinzuefüege", - "add_to": "Hinzuefüege zu …", - "add_to_album": "Zum Album hinzuefüege", - "add_to_album_bottom_sheet_added": "Zu {album} hinzuegfüegt", - "add_to_album_bottom_sheet_already_exists": "Scho in {album}", - "add_to_album_bottom_sheet_some_local_assets": "Es hend es paar lokali Dateie nöd chöne im Album hinzuegfüegt werde", - "add_to_album_toggle": "Uuswahl umschalte für {album}", - "add_to_albums": "Zu Albe hinzuefüege", - "add_to_albums_count": "Zu Albe hinzuefüege ({count})", - "add_to_bottom_bar": "Hinzuefüege zu", - "add_to_shared_album": "Zum teilte Album hinzuefüege", - "add_upload_to_stack": "Upload zum Stack hinzuefüege", - "add_url": "URL hinzuefüege", - "add_workflow_step": "Workflow-Schritt hinzuefüege", - "added_to_archive": "Is Archiv verschobe", - "added_to_favorites": "Zu dine Favoritä hinzuegfüegt", - "added_to_favorites_count": "{count, number} zu Favorite hinzuegfüegt", + "add_exclusion_pattern": "Ausschlussmuster hinzufügen", + "add_filter": "Filter hinzufügen", + "add_filter_description": "Klicke hier um eine Filterbedingung hinzuzufügen", + "add_location": "Standort hinzufügen", + "add_more_users": "Mehr Benutzer hinzufügen", + "add_partner": "Partner hinzufügen", + "add_path": "Pfad hinzufügen", + "add_photos": "Fotos hinzufügen", + "add_tag": "Tag hinzufügen", + "add_to": "Hinzufügen zu…", + "add_to_album": "Zu Album hinzufügen", + "add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt", + "add_to_album_bottom_sheet_already_exists": "Bereits in {album}", + "add_to_album_bottom_sheet_some_local_assets": "Einige lokale Dateien konnten nicht zum Album hinzugefügt werden", + "add_to_album_toggle": "Auswahl umschalten für {album}", + "add_to_albums": "Zu Alben hinzufügen", + "add_to_albums_count": "Zu Alben hinzufügen ({count})", + "add_to_bottom_bar": "Hinzufügen zu", + "add_to_shared_album": "Zu geteiltem Album hinzufügen", + "add_upload_to_stack": "Upload zum Stapel hinzufügen", + "add_url": "URL hinzufügen", + "add_workflow_step": "Workflow-Schritt hinzufügen", + "added_to_archive": "Zum Archiv hinzugefügt", + "added_to_favorites": "Zu Favoriten hinzugefügt", + "added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt", "admin": { - "add_exclusion_pattern_description": "Uusschlussmuster hinzuefüge. Platzhalter, wie *, **, und ? wärded understützt. Zum all Dateie i eim Verzeichnis namens „Raw\" ignoriere, „**/Raw/**“ verwände. Zum all Dateien ignorieren, wo uf „.tif“ änded, „**/*.tif“ verwände. Zum en absolute Pfad ignoriere, „/pfad/zum/ignoriere/**“ verwände.", - "admin_user": "Admin Benutzer", - "asset_offline_description": "Die Datei vonere externe Bibliothek isch nümme uf de Festplatte und isch in Papierchorb verschobe worde. Falls die Datei innerhalb vo de Bibliothek verschoben worde isch, überprüf dini Ziitleiste uf die neui entsprechendi Datei. Zum die Datei wiederherstelle, stell bitte sicher, dass Immich uf de unde stehendi Dateipfad chan zuegriife und scann d'Bibliothek.", - "authentication_settings": "Authentifizierigs Iistellige", - "authentication_settings_description": "Passwort, OAuth und anderi Authentifizierigseinstellige verwalte", - "authentication_settings_disable_all": "Bisch sicher, dass du alli Login-Methodä wotsch deaktivierä? S Login isch denn komplett deaktiviert.", - "authentication_settings_reenable": "Bruuch ein Server-Befehl zum reaktiviere.", - "background_task_job": "Hintergrund Ufgabä", - "backup_database": "Datenbank-Dump aalege", - "backup_database_enable_description": "Datenbank-Dumps aktiviere", - "backup_keep_last_amount": "Aazahl vo de vorherige Dumps, wo bhalte werde sölle", - "backup_onboarding_1_description": "Offsite-Kopie i dä Cloud oder amene andere physische Standort.", - "backup_onboarding_2_description": "Lokali Kopie uf verschiedene Grät. Das beinhaltet d Hauptdateie und e lokali Sicherig vo dene Dateie.", - "backup_onboarding_3_description": "Total aazahl vo dine Dateikopie, inklusiv d Originaldateie. Das beinhaltet 1 Offsite-Kopie und 2 lokali Kopie.", - "backup_onboarding_description": "E 3-2-1-Backup-Strategie wird empfohle, zum dini Dateie z schütze. Du söttsch sowohl Kopie vo dine ufgeladene Fotos/Videos wie au d Immich-Datenbank bhalte, für e rundum sauberi Backup-Lösig.", - "backup_onboarding_footer": "Für meh Infos zum Backup vo Immich lueg bitte i d Dokumentation.", - "backup_onboarding_parts_title": "Es 3-2-1-Backup beinhaltet:", + "add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw“ zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.", + "admin_user": "Administrator", + "asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.", + "authentication_settings": "Authentifizierungseinstellungen", + "authentication_settings_description": "Passwort-, OAuth- und andere Authentifizierungseinstellungen verwalten", + "authentication_settings_disable_all": "Bist du sicher, dass du alle Loginmethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.", + "authentication_settings_reenable": "Nutze einen Server-Befehl zur Reaktivierung.", + "background_task_job": "Hintergrundaufgaben", + "backup_database": "Datenbanksicherung erstellen", + "backup_database_enable_description": "Datenbank regelmässig sichern", + "backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen", + "backup_onboarding_1_description": "Offsite-Kopie in der Cloud oder an einem anderen physischen Ort.", + "backup_onboarding_2_description": "lokale Kopien auf verschiedenen Geräten. Dazu gehören die Hauptdateien und eine lokale Sicherung dieser Dateien.", + "backup_onboarding_3_description": "Kopien deiner Daten inklusive Originaldateien. Dies umfasst 1 Kopie an einem anderen Ort und 2 lokale Kopien.", + "backup_onboarding_description": "Eine 3-2-1 Sicherungsstrategie wird empfohlen, um deine Daten zu schützen. Du solltest sowohl Kopien deiner hochgeladenen Fotos/Videos als auch der Immich-Datenbank aufbewahren, um eine umfassende Sicherungslösung zu haben.", + "backup_onboarding_footer": "Weitere Informationen zum Sichern von Immich findest du in der Dokumentation.", + "backup_onboarding_parts_title": "Eine 3-2-1-Sicherung umfasst:", "backup_onboarding_title": "Backups", - "backup_settings": "Iistellige für Datenbank-Dumps", - "backup_settings_description": "Datenbank-Dump-Iistellige verwalte.", - "cleared_jobs": "Jobs glöscht für: {job}", - "config_set_by_file": "D Konfiguration isch aktuell dur e Konfigurationsdatei gsetzt", - "confirm_delete_library": "Bisch sicher, dass du d Bibliothek {library} wotsch lösche?", - "confirm_delete_library_assets": "Bisch sicher, dass du die Bibliothek wotsch lösche? Das löscht {count, plural, one {# enthaltenes Asset} other {alli # enthaltene Assets}} us Immich und chan nöd rückgängig gmacht werde. D Dateie bliibed uf em Dateträger.", - "confirm_email_below": "Zum bestätige bitte \"{email}\" une iitippe", - "confirm_reprocess_all_faces": "Bisch sicher, dass du alli Gsichter neu verarbeite wotsch? Däbii werde au benannti Persone glöscht.", - "confirm_user_password_reset": "Bisch sicher, dass du s Passwort für {user} möchtisch zruggsetze?", - "confirm_user_pin_code_reset": "Bisch sicher, dass du de PIN-Code vo {user} möchtisch zruggsetze?", - "copy_config_to_clipboard_description": "Kopier die aktuelli Systemkonfiguration als JSON-Objekt i d'Zwüschenablage", - "create_job": "Uufgabe erstelle", - "cron_expression": "Cron-Ziitagabe", - "cron_expression_description": "Setz s Scanintervall im Cron-Format. Hilf mit däm Format bütet z. B. der Crontab Guru", - "cron_expression_presets": "Vorlage für Cron-Uusdruck", - "disable_login": "Login deaktiviere", - "duplicate_detection_job_description": "Die Uufgab füehrt s maschinelle Lärne für jedi Datei us, zum Duplikat finde. Die Uufgabe berueht uf de intelligente Suechi", - "exclusion_pattern_description": "Mit Uusschlussmuster chönnd Dateie und Ordner bim Scanne vo dinere Bibliothek ignoriert wärde. Das isch nützlich, wenn du Ordner häsch, wo Dateien drin händ, wo d nöd wotsch importiere, wie z. B. RAW-Dateie.", - "export_config_as_json_description": "Lad die aktuelli Systemkonfiguration als JSON-Datei abe", - "external_libraries_page_description": "Externi Bibliothekssiite für Administratore", - "face_detection": "Gsichtserkennig", - "face_detection_description": "Die Uufgab erfasst Gsichter in Dateien dur maschinells Lerne. Bi Video wird nur d'Miniaturasicht brucht. „Aktualisiere“ verarbeitet all Dateie neu. „Zruggsetze“ setzt au no all Gsichter zrugg. „Fehlendi“ stellt nur nöd verarbeiteti Dateie in d'Warteschlange. Erfassti Gsichter wärdet zur Gsichtsidentifizierig in diWarteschlange gstellt, damit sie i bestehendi oder neui Persone z'gruppiere.", - "facial_recognition_job_description": "Die Uufgabe gruppiert im Anschluss an d'Gsichtserfassig die erfasste Gsichter zu Persone. „Zruggsetze“ gruppiert alli Gsichter neu und mit „Fehlendi“ werdet Gsichter ohni Zuordnig i d'Warteschlange gstellt.", - "failed_job_command": "Befehl {command} hät für d'Uufgabe {job} nöd funktioniert", - "force_delete_user_warning": "WARNIG: Die Aktion löscht dä Benutzer und all sini Dateie. Das chann nöd rückgängig gmacht wärde und d'Dateie chönnd nöd wiederhergstellt wärde.", + "backup_settings": "Einstellungen für Datenbanksicherung", + "backup_settings_description": "Einstellungen zur regelmässigen Sicherung der Datenbank.", + "cleared_jobs": "Folgende Aufgaben zurückgesetzt: {job}", + "config_set_by_file": "Die Konfiguration ist aktuell durch eine Konfigurationsdatei gsetzt", + "confirm_delete_library": "Bist du sicher, dass du die Bibliothek {library} löschen willst?", + "confirm_delete_library_assets": "Bist du sicher, dass du diese Bibliothek löschen willst? Dies löscht {count, plural, one {# enthaltene Datei} other {alle # enthaltenen Dateien}} aus Immich und kann nicht rückgängig gemacht werden. Die Dateien bleiben auf der Festplatte erhalten.", + "confirm_email_below": "Zum Bestätigen, tippe unten \"{email}\" ein", + "confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.", + "confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?", + "confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN-Code von {user} zurücksetzen möchtest?", + "copy_config_to_clipboard_description": "Aktuelle Systemkonfiguration als JSON-Objekt in die Zwischenablage kopieren", + "create_job": "Aufgabe erstellen", + "cron_expression": "Cron-Ausdruck", + "cron_expression_description": "Setze das Scanintervall im Cron-Format. Für mehr Informationen, siehe z. B. Crontab Guru", + "cron_expression_presets": "Vorlagen für Cron-Ausdrücke", + "disable_login": "Login deaktivieren", + "duplicate_detection_job_description": "Verwendet maschinelles Lernen auf den Dateien, um Duplikate zu finden. Baut auf der intelligenten Suche auf", + "exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen deiner Bibliothek ignoriert werden. Dies ist nützlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren möchtest, wie z. B. RAW-Dateien.", + "export_config_as_json_description": "Aktuelle Systemkonfiguration als JSON-Datei herunterladen", + "external_libraries_page_description": "Externe Bibliotheksseite für Administratoren", + "face_detection": "Gesichtserkennung", + "face_detection_description": "Diese Aufgabe erkennt mit maschinellem Lernen Gesichter in Dateien. Bei Videos wird nur das Vorschaubild verwendet. „Aktualisieren“ verarbeitet alle Dateien neu. „Zurücksetzen“ setzt zusätzlich alle Gesichter zurück. „Fehlende“ fügt nur nicht verarbeitete Dateien in die Warteschlange ein. Erfasste Gesichter werden zur Gesichtsidentifizierung in die Warteschlange eingefügt, um sie in bestehende oder neue Personen zu gruppieren.", + "facial_recognition_job_description": "Diese Aufgabe gruppiert im Anschluss an die Gesichtserkennung die erkannten Gesichter zu Personen. „Zurücksetzen“ gruppiert alle Gesichter neu, während „Fehlende“ Gesichter ohne Zuordnung in die Warteschlange stellt.", + "failed_job_command": "Befehl {command} ist für Aufgabe {job} fehlgeschlagen", + "force_delete_user_warning": "WARNUNG: Diese Aktion löscht sofort den Benutzer und all seine Dateien. Dies kann nicht rückgängig gemacht werden und die Dateien können nicht wiederhergestellt werden.", "image_format": "Format", - "image_format_description": "WebP erzeugt chlineri Dateie we JPEG, isch aber es bitz langsamer i de Erstellig.", - "image_fullsize_description": "Hochuflösends Bild mit glöschte Metadate, wo bim Zoome brucht wird", - "image_fullsize_enabled": "Hochuflösendi Vorschaubilder aktiviere", - "image_fullsize_enabled_description": "Generiere hochauflösende Vorschaubilder in Originalauflösung für nicht web-kompatibel Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.", + "image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist aber etwas langsamer in der Erstellung.", + "image_fullsize_description": "Hochauflösendes Bild mit entfernten Metadaten, das beim Zoomen verwendet wird", + "image_fullsize_enabled": "Hochauflösende Vorschaubilder aktivieren", + "image_fullsize_enabled_description": "Generiere Vorschaubilder in Originalauflösung für nicht web-kompatible Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.", "image_fullsize_quality_description": "Qualität der hochauflösenden Vorschaubilder von 1-100. Höher ist besser, erzeugt aber grössere Dateien.", "image_fullsize_title": "Hochauflösende Vorschaueinstellungen", "image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen", "image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.", "image_prefer_wide_gamut": "Breites Spektrum bevorzugen", - "image_prefer_wide_gamut_setting_description": "Bruuch Display P3 für Vorschaubildli. Das erhaltet d'Vitalität von Bildli mit grossem Farbruum besser. Uf alte Grät mit alte Browser chann das aber andersch uusgseh. sRGB-Bildli wärdet als sRGB bhalte zum Farbänderige vermiide.", - "image_preview_description": "Mittelgrossi Bildli ohni Metadate, bruuchts für Einzelaasichte und fürs maschinelle Lärne", - "image_preview_quality_description": "Vorschauqualität vo 1-100. Höcher isch besser, git aber grösseri Dateie und chan d'App Schwuppdizität reduziere. Z tüffi Wert chönnd s maschinelle Lärne beiträchtige.", - "image_preview_title": "Vorschauiistellige", + "image_prefer_wide_gamut_setting_description": "Display P3 (DCI-P3) für Vorschaubilder verwenden. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.", + "image_preview_description": "Mittelgrosses Bild mit entfernten Metadaten, das bei der Betrachtung einer einzelnen Datei und für maschinelles Lernen verwendet wird", + "image_preview_quality_description": "Vorschauqualität von 1-100. Ein höherer Wert ist besser, erzeugt dadurch aber grössere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen. Ein niedriger Wert kann dafür aber die Qualität des maschinellen Lernens beeinträchtigen.", + "image_preview_title": "Vorschaueinstellungen", "image_progressive": "Fortlaufend", - "image_progressive_description": "Codier fortlaufendi JPEG-Bildi: Sie wärdet bim Lade aufbauend aazeiget. Das hät kei Würkig uf WebP-Bildi.", + "image_progressive_description": "JPEG-Bilder schrittweise kodieren, um ein stufenweises Laden zu ermöglichen. Dies hat keine Auswirkungen auf WebP-Bilder.", "image_quality": "Qualität", - "image_resolution": "Uuflösig", - "image_resolution_description": "Höcheri Uuflösig erhaltet meh Detail, gaht aber länger zum codiere, macht grösseri Dateie und chan d'App Schuppdizität reduziere.", - "image_settings": "Bild-Iistellige", - "image_settings_description": "Qualität und Uuflösig von erstellte Bildli verwalte", - "image_thumbnail_description": "Chlini Vorschaubildli ohni Metadate, bruuchts für Aasichte mit Gruppe vo Föteli wie i de Hauptziitachse", - "image_thumbnail_quality_description": "Vorschauqualität vo 1-100. Höcher isch besser, git aber grösseri Dateie und chan d'App Schwuppdizität reduziere.", - "image_thumbnail_title": "Iistellige für Vorschaubildli", - "import_config_from_json_description": "Systemkonfiguration importiere durs Ufelade vonere JSON-Datei", - "job_concurrency": "{job} Näbeläufigkeit", - "job_created": "Uufgab erstellt", - "job_not_concurrency_safe": "Die Uufgabe ist nöd für Paralleluusführig gmacht.", - "job_settings": "Uufgabe-Iistellige", - "job_settings_description": "Uufgabe-Näbeläufigkeit verwalte", - "jobs_over_time": "Uufgabe in ziitliche Verlauf", + "image_resolution": "Auflösung", + "image_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Kodierung, haben grössere Dateigrössen und können die Reaktionsfähigkeit der App beeinträchtigen.", + "image_settings": "Bildeinstellungen", + "image_settings_description": "Qualität und Auflösung der generierten Bilder verwalten", + "image_thumbnail_description": "Kleines Vorschaubild mit entfernten Metadaten, die bei der Anzeige von Sammlungen von Fotos wie der Zeitleiste verwendet wird", + "image_thumbnail_quality_description": "Qualität der Vorschaubilder von 1-100. Höher ist besser, erzeugt aber grössere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen.", + "image_thumbnail_title": "Einstellungen für Vorschaubilder", + "import_config_from_json_description": "Systemkonfiguration von hochgeladener JSON-Konfigurationsdatei importieren", + "job_concurrency": "{job} (Anzahl gleichzeitig laufende Prozesse)", + "job_created": "Aufgabe erstellt", + "job_not_concurrency_safe": "Diese Aufgabe kann nicht mehrmals parallel laufen gelassen werden.", + "job_settings": "Aufgabeneinstellungen", + "job_settings_description": "Gleichzeitige Ausführung von Aufgaben verwalten", + "jobs_over_time": "Jobs im Laufe der Zeit", "library_created": "Bibliothek erstellt: {library}", - "library_deleted": "Bibliothek glöscht", - "library_details": "Bibliotheks-Details", - "library_folder_description": "Gib en Order zum Importiere a. Dä Order mit sine Underordner wird nach Bildli und Videos durchsucht.", - "library_remove_exclusion_pattern_prompt": "Bisch sicher, dass das Uuschluss-Muster wotsch lösche?", - "library_remove_folder_prompt": "Bisch sicher, dass dä Import-Ordner wotsch lösche?", - "library_scanning": "Regelmässigi Überprüefig" + "library_deleted": "Bibliothek gelöscht", + "library_details": "Bibliotheksdetails", + "library_folder_description": "Wähle einen Ordner zum Importieren. Dieser Ordner wird inklusive Unterordnern nach Bildern und Videos durchsucht.", + "library_remove_exclusion_pattern_prompt": "Bilst du sicher, dass du dieses Ausschlussmuster entfernen möchtest?", + "library_remove_folder_prompt": "Bist du sicher, dass du diesen Import-Ordner entfernen möchtest?", + "library_scanning": "Regelmässiges Scannen" } } diff --git a/i18n/en.json b/i18n/en.json index 4f2922f35d..4f608f890d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -267,6 +267,8 @@ "notification_enable_email_notifications": "Enable email notifications", "notification_settings": "Notification Settings", "notification_settings_description": "Manage notification settings, including email", + "oauth_allow_insecure_requests": "Allow insecure requests", + "oauth_allow_insecure_requests_description": "WARNING: This disables TLS certificate validation for OAuth requests and may expose you to MITM attacks.", "oauth_auto_launch": "Auto launch", "oauth_auto_launch_description": "Start the OAuth login flow automatically upon navigating to the login page", "oauth_auto_register": "Auto register", @@ -1392,6 +1394,7 @@ "light_theme": "Switch to light theme", "like": "Like", "like_deleted": "Like deleted", + "link": "Link", "link_motion_video": "Link motion video", "link_to_docs": "For more information, refer to the documentation.", "link_to_oauth": "Link to OAuth", @@ -1562,6 +1565,8 @@ "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "mute_memories": "Mute Memories", "my_albums": "My albums", + "my_immich_description": "Copy current page as a My Immich link", + "my_immich_title": "My Immich link", "name": "Name", "name_or_nickname": "Name or nickname", "name_required": "Name is required", @@ -1926,6 +1931,8 @@ "scan_settings": "Scan Settings", "scanning": "Scanning", "scanning_for_album": "Scanning for album...", + "screencast_mode_description": "Show keyboard and mouse event indicators on the screen", + "screencast_mode_title": "Toggle screencast mode", "search": "Search", "search_albums": "Search albums", "search_by_context": "Search by context", @@ -2214,6 +2221,8 @@ "sync_status": "Sync Status", "sync_status_subtitle": "View and manage the sync system", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "system_theme": "System theme", + "system_theme_command_description": "Use the system theme ({value})", "tag": "Tag", "tag_assets": "Tag assets", "tag_created": "Created tag: {tag}", diff --git a/i18n/eo.json b/i18n/eo.json index 087b2dcc5e..5bc956b284 100644 --- a/i18n/eo.json +++ b/i18n/eo.json @@ -849,9 +849,12 @@ "create_link_to_share": "Krei ligilon por dividi", "create_link_to_share_description": "Permesi, ke iu ajn kun la ligilo povu vidi la elektita(j)n foto(j)n", "create_new": "KREI NOVAN", + "create_new_face": "Krei novan vizaĝon", "create_new_person": "Krei novan homon", "create_new_person_hint": "Atribui elektitajn elementojn al nova homo", "create_new_user": "Krei novan uzanton", + "create_person": "Krei homon", + "create_person_subtitle": "Aldoni nomon al la elektita vizaĝo por krei kaj etikedi novan homon", "create_shared_album_page_share_add_assets": "ALDONI ELEMENTOJN", "create_shared_album_page_share_select_photos": "Elekti fotojn", "create_shared_link": "Krei dividitan ligilon", @@ -1043,7 +1046,7 @@ "cannot_navigate_previous_asset": "Ne eblas navigi al antaŭa elemento", "cant_apply_changes": "Ne eblas apliki ŝanĝojn", "cant_change_activity": "Ne eblas {enabled, select, true {malŝalti} other {ŝalti}} tiun agon", - "cant_change_asset_favorite": "Ne eblas ŝanĝi preferaton por tiu elemento", + "cant_change_asset_favorite": "Ne eblas ŝanĝi preferon por tiu elemento", "cant_change_metadata_assets_count": "Ne eblas ŝanĝi metadatumojn de {count, plural, one {# elemento} other {# elementoj}}", "cant_get_faces": "Ne eblas trovi vizaĝojn", "cant_get_number_of_comments": "Ne eblas trovi nombron da komentoj", @@ -1074,7 +1077,18 @@ "incorrect_email_or_password": "Neĝusta retadreso aŭ pasvorto", "library_folder_already_exists": "Tiu ĉi import-vojo jam ekzistas.", "page_not_found": "Paĝo ne trovita", + "paths_validation_failed": "Evidentiĝis, ke {paths, plural, one {# vojo estas nevalida} other {# vojoj estas nevalidaj}}", + "profile_picture_transparent_pixels": "Ne eblas havi travideblaj bilderoj en profilbildo. Bonvolu zomi kaj/aŭ ŝovi la bildon al loko sen tiaj bilderoj.", + "quota_higher_than_disk_size": "Vi donis kvoton pli grandan ol la disko mem", + "something_went_wrong": "Io misis", + "unable_to_add_album_users": "Ne eblas aldoni uzantojn al la albumo", + "unable_to_add_assets_to_shared_link": "Ne eblas aldoni elementojn al la dividita ligilo", + "unable_to_add_comment": "Ne eblas aldoni komenton", "unable_to_add_exclusion_pattern": "Ne eblas aldoni skemon de ekskludo", + "unable_to_add_partners": "Ne eblas aldoni partnerojn", + "unable_to_add_remove_archive": "Ne eblas {archived, select, true {forigi elementon de} other {aldoni elementon al}} la arĥivo", + "unable_to_add_remove_favorites": "Ne eblas {favorite, select, true {aldoni elementon al} other {forigi elementon de}} preferataĵoj", + "unable_to_change_favorite": "Ne eblas ŝanĝi preferon por tiu elemento", "unable_to_create": "Ne eblis krei laborfluon", "unable_to_delete_exclusion_pattern": "Ne eblas forigi skemon de ekskludo", "unable_to_delete_workflow": "Ne eblis forigi laborfluon", @@ -1088,15 +1102,25 @@ "expand_all": "Etendi ĉiujn", "explore": "Esplori", "explorer": "Foliumilo", + "favorite": "Preferataĵo", + "favorite_action_prompt": "{count} aldonita(j) al Preferataĵoj", + "favorite_or_unfavorite_photo": "Aldoni/forigi foton al/de preferataĵoj", + "favorites": "Preferataĵoj", + "favorites_page_no_favorites": "Neniuj preferataj elementoj trovitaj", "free_up_space": "Liberigi spacon", "free_up_space_description": "Vi forigos fotojn kaj/aŭ videojn, kiuj havas savkopiojn en la servilo, por liberigi spacon en via aparato. La kopioj en la servilo restos.", "general": "Ĝeneralaj", + "home_page_favorite_err_local": "Ankoraŭ ne eblas aldoni lokajn elementojn al Preferataĵoj; ignorita(j)", + "home_page_favorite_err_partner": "Ankoraŭ ne eblas aldoni elementojn de partnero al Preferataĵoj; ignorita(j)", + "keep_favorites": "Konservi preferataĵojn", "manage_media_access_settings": "Malfermi agordaĵaron", "manage_the_app_settings": "Agordi la apon", + "map_settings_only_show_favorites": "Montri nur preferataĵojn", "missing": "Netraktitaj", "networking_subtitle": "Administri agordojn pri finpunktoj de la servilo", "no_devices": "Neniuj aprobitaj aparatoj", "no_explore_results_message": "Alŝutu pli da fotoj por esplori vian kolekton.", + "no_favorites_message": "Aldoni al Preferataĵoj por rapide retrovi viajn plej bonajn bildojn kaj videojn", "no_notifications": "Neniuj sciigoj", "no_results_description": "Provu sinonimon aŭ pli ĝeneralan ŝlosilvorton", "notification_permission_dialog_content": "Por ŝalti sciigojn, iru al Agordoj kaj elektu 'permesi'.", @@ -1106,10 +1130,14 @@ "notification_toggle_setting_description": "Ŝalti sciigojn per retmesaĝo", "notifications": "Sciigoj", "notifications_setting_description": "Administri sciigojn", + "only_favorites": "Nur preferataĵoj", "preferences_settings_subtitle": "Administri agordojn pri la apo", "purchase_settings_server_activated": "La administranto respondecas pri la ŝlosilo de aŭtentikeco por la servilo", "rating_clear": "Forviŝi pritakson", "refresh": "Denove", + "remove_from_favorites": "Forigi el preferataĵoj", + "removed_from_favorites": "Forigita(j) el preferataĵoj", + "removed_from_favorites_count": "{count, plural, other {Forigis #}} el Preferataĵoj", "rescan": "Reanalizi", "reset": "Restartigi", "reset_sqlite_clear_app_data": "Forviŝi datumojn", @@ -1127,7 +1155,10 @@ "setting_notifications_subtitle": "Redakti viajn preferojn pri sciigoj", "start_date": "Komenca dato", "start_date_before_end_date": "Komenca dato devas esti antaŭ fina dato", + "to_favorite": "Aldoni al preferataĵoj", "trigger_description": "Evento, kiu ekfunkciigas la laborfluon", + "unfavorite": "Forigi el preferataĵoj", + "unfavorite_action_prompt": "{count} forigita(j) el Preferataĵoj", "untitled_workflow": "Sentitola laborfluo", "upload_concurrency": "Nombro da samtempaj alŝutoj", "user_pin_code_settings_description": "Administri vian PIN-kodon", diff --git a/i18n/hr.json b/i18n/hr.json index 5b8f19d174..0ed46addf9 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -845,9 +845,12 @@ "create_link_to_share": "Izradite vezu za dijeljenje", "create_link_to_share_description": "Dopusti svakome s vezom da vidi odabrane fotografije", "create_new": "KREIRAJ NOVO", + "create_new_face": "Stvori novo lice", "create_new_person": "Stvorite novu osobu", "create_new_person_hint": "Dodijelite odabrane stavke novoj osobi", "create_new_user": "Kreiraj novog korisnika", + "create_person": "Stvori novu osobu", + "create_person_subtitle": "Dodaj ime odabranom licu kako bi stvorio i tagirao novu osobu", "create_shared_album_page_share_add_assets": "DODAJ STAVKE", "create_shared_album_page_share_select_photos": "Odaberi fotografije", "create_shared_link": "Kreiraj dijeljeni link", @@ -922,6 +925,7 @@ "deselect_all": "Poništi odabir svih", "details": "Detalji", "direction": "Smjer", + "disable": "Onesposobi", "disabled": "Onemogućeno", "disallow_edits": "Zabrani izmjene", "discord": "Discord", @@ -947,6 +951,7 @@ "download_include_embedded_motion_videos": "Ugrađeni videozapisi", "download_include_embedded_motion_videos_description": "Uključite videozapise ugrađene u fotografije s pokretom kao zasebnu datoteku", "download_notfound": "Preuzimanje nije pronađeno", + "download_original": "Preuzmi original", "download_paused": "Preuzimanje pauzirano", "download_settings": "Preuzmi", "download_settings_description": "Upravljajte postavkama vezanim uz preuzimanje stavki", @@ -956,10 +961,11 @@ "download_waiting_to_retry": "Čeka se ponovni pokušaj", "downloading": "Preuzimanje", "downloading_asset_filename": "Preuzimanje stavke {filename}", + "downloading_from_icloud": "Preuzmi s iCloud", "downloading_media": "Preuzimanje medija", "drop_files_to_upload": "Ispustite datoteke bilo gdje za prijenos", "duplicates": "Duplikati", - "duplicates_description": "Razriješite svaku grupu tako da naznačite koji su duplikati, ako ih ima", + "duplicates_description": "Razriješite svaku grupu tako da naznačite koji su duplikati, ako ih ima.", "duration": "Trajanje", "edit": "Izmjena", "edit_album": "Uredi album", @@ -987,6 +993,12 @@ "editor": "Urednik", "editor_close_without_save_prompt": "Promjene neće biti spremljene", "editor_close_without_save_title": "Zatvoriti uređivač?", + "editor_confirm_reset_all_changes": "Jeste li sigurni da želite resetirati sve opcije?", + "editor_discard_edits_confirm": "Odbaci izmjene", + "editor_discard_edits_prompt": "Imate nesačuvane izmjene. Jeste li sigurni da ih želite odbaciti?", + "editor_discard_edits_title": "Odbaci izmjene?", + "editor_rotate_left": "Rotiraj 90° u suprotnom smjeru kazaljke na satu", + "editor_rotate_right": "Rotiraj 90° u smjeru kazaljke na satu", "email": "E-pošta", "email_notifications": "Obavijesti putem e-maila", "empty_folder": "Ova mapa je prazna", diff --git a/i18n/id.json b/i18n/id.json index a6f0b649b9..f2d34116cb 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -849,9 +849,12 @@ "create_link_to_share": "Buat tautan untuk dibagikan", "create_link_to_share_description": "Biarkan siapa pun dengan tautan melihat foto yang dipilih", "create_new": "BUAT BARU", + "create_new_face": "Buat wajah baru", "create_new_person": "Buat orang baru", "create_new_person_hint": "Tetapkan aset yang dipilih ke orang yang baru", "create_new_user": "Buat pengguna baru", + "create_person": "Buat orang", + "create_person_subtitle": "Tambahkan nama pada wajah yang dipilih untuk membuat dan menandai orang baru", "create_shared_album_page_share_add_assets": "TAMBAHKAN ASET", "create_shared_album_page_share_select_photos": "Pilih Foto", "create_shared_link": "Buat tautan bersama", @@ -2214,6 +2217,7 @@ "tag": "Tag", "tag_assets": "Tag aset", "tag_created": "Tag yang dibuat: {tag}", + "tag_face": "Tandai wajah", "tag_feature_description": "Menjelajahi foto dan video yang dikelompokkan berdasarkan topik tag yang logis", "tag_not_found_question": "Tidak dapat menemukan tag? Buat tag baru.", "tag_people": "Beri Tag Orang", diff --git a/i18n/ko.json b/i18n/ko.json index 7114316134..fff2c3d10a 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -1934,6 +1934,7 @@ "search_by_filename": "파일명 또는 확장자로 검색", "search_by_filename_example": "예: IMG_1234.JPG 또는 PNG", "search_by_ocr": "OCR로 검색", + "search_by_ocr_example": "라떼", "search_camera_lens_model": "렌즈 모델 검색...", "search_camera_make": "카메라 제조사 검색...", "search_camera_model": "카메라 모델명 검색...", @@ -1997,6 +1998,7 @@ "select_all_in": "{group}의 모든 항목 선택", "select_avatar_color": "아바타 색상 선택", "select_count": "{count, plural, one {# 선택중} other {# 선택중}}", + "select_cutoff_date": "유지 기간 설정", "select_face": "얼굴 선택", "select_featured_photo": "대표 사진 선택", "select_from_computer": "컴퓨터에서 선택", diff --git a/i18n/lt.json b/i18n/lt.json index b686c37b83..b686e2526d 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -1215,7 +1215,7 @@ "file_name_text": "Failo pavadinimas", "file_name_with_value": "Failo pavadinimas: {file_name}", "file_size": "Failo dydis", - "filename": "Failopavadinimas", + "filename": "Failo pavadinimas", "filetype": "Failo tipas", "filter": "Filtras", "filter_description": "Tikslinių elementų filtravimo sąlygos", @@ -1390,8 +1390,8 @@ "licenses": "Licencijos", "light": "Šviesi", "light_theme": "Perjungti į šviesią temą", - "like": "Kaip", - "like_deleted": "Kaip ištrintas", + "like": "Patinka", + "like_deleted": "Patinka panaikintas", "link_motion_video": "Susieti judesio vaizdo įrašą", "link_to_docs": "Daugiau informacijos rasite dokumentacijoje.", "link_to_oauth": "Susieti su OAuth", diff --git a/i18n/lv.json b/i18n/lv.json index 8ad03da2b8..0c7776efe7 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -713,9 +713,11 @@ "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopīgošanas saiti", "create_new": "IZVEIDOT JAUNU", + "create_new_face": "Izveidot jaunu seju", "create_new_person": "Izveidot jaunu personu", "create_new_person_hint": "Piesaistīt izvēlētos failus jaunai personai", "create_new_user": "Izveidot jaunu lietotāju", + "create_person": "Izveidot personu", "create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS", "create_shared_album_page_share_select_photos": "Fotoattēlu Izvēle", "create_user": "Izveidot lietotāju", @@ -879,6 +881,7 @@ "failed_to_update_notification_status": "Neizdevās mainīt paziņojuma statusu", "incorrect_email_or_password": "Nepareizs e-pasts vai parole", "library_folder_already_exists": "Šis importa ceļš jau pastāv.", + "page_not_found": "Lapa nav atrasta", "profile_picture_transparent_pixels": "Profila attēlos nevar būt caurspīdīgi pikseļi. Lūdzu, palielini un/vai pārvieto attēlu.", "quota_higher_than_disk_size": "Tu esi iestatījis kvotu, kas pārsniedz diska izmēru", "something_went_wrong": "Kaut kas nogāja greizi", @@ -1295,6 +1298,7 @@ "only_favorites": "Tikai izlase", "open": "Atvērt", "open_calendar": "Atvērt kalendāru", + "open_in_browser": "Atvērt pārlūkprogrammā", "open_in_map_view": "Atvērt kartes skatā", "open_in_openstreetmap": "Atvērt OpenStreetMap", "open_the_search_filters": "Atvērt meklēšanas filtrus", @@ -1455,6 +1459,7 @@ "reset_people_visibility": "Atiestatīt personu redzamību", "reset_pin_code": "Atiestatīt PIN kodu", "reset_sqlite": "Atiestatīt SQLite datubāzi", + "reset_sqlite_clear_app_data": "Notīrīt datus", "reset_to_default": "Atiestatīt noklusējuma iestatījumus", "resolve_duplicates": "Atrisināt dublēšanās gadījumus", "resolved_all_duplicates": "Visi dublikāti ir atrisināti", @@ -1705,6 +1710,7 @@ "sync_local": "Sinhronizēt lokāli", "sync_status": "Sinhronizācijas statuss", "sync_status_subtitle": "Skatīt un pārvaldīt sinhronizācijas sistēmu", + "tag_face": "Atzīmēt seju", "text_recognition": "Teksta atpazīšana", "theme": "Dizains", "theme_setting_asset_list_storage_indicator_title": "Rādīt krātuves indikatoru uz attēliem režga skatā", @@ -1833,6 +1839,7 @@ "viewer_remove_from_stack": "Noņemt no Steka", "viewer_stack_use_as_main_asset": "Izmantot kā Galveno Aktīvu", "viewer_unstack": "At-Stekot", + "visibility": "Redzamība", "visual": "Vizuāli", "visual_builder": "Vizuālais veidotājs", "waiting": "Gaida", diff --git a/i18n/nl.json b/i18n/nl.json index 951638d470..c584fc4b86 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -544,7 +544,7 @@ "appears_in": "Komt voor in", "apply_count": "Toepassen ({count, number})", "archive": "Archief", - "archive_action_prompt": "{count} item(s) toegevoegd aan het archief", + "archive_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan het archief", "archive_or_unarchive_photo": "Foto archiveren of uit het archief halen", "archive_page_no_archived_assets": "Geen gearchiveerde items gevonden", "archive_page_title": "Archief ({count})", @@ -593,20 +593,20 @@ "assets_cannot_be_added_to_album_count": "{count, plural, one {# item} other {# items}} konden niet aan album toegevoegd worden", "assets_cannot_be_added_to_albums": "{count, plural, one {Item kan} other {Items kunnen}} niet toegevoegd worden aan de albums", "assets_count": "{count, plural, one {# item} other {# items}}", - "assets_deleted_permanently": "{count} item(s) permanent verwijderd", - "assets_deleted_permanently_from_server": "{count} item(s) permanent verwijderd van de Immich server", + "assets_deleted_permanently": "{count, plural, one {# item} other {# items}} permanent verwijderd", + "assets_deleted_permanently_from_server": "{count, plural, one {# item} other {# items}} permanent verwijderd van de Immich server", "assets_downloaded_failed": "{count, plural, one {# bestand gedownload - {error} bestand mislukt} other {# bestanden gedownload - {error} bestanden mislukt}}", "assets_downloaded_successfully": "{count, plural, one {# bestand succesvol gedownload} other {# bestanden succesvol gedownload}}", "assets_moved_to_trash_count": "{count, plural, one {# item} other {# items}} verplaatst naar prullenbak", "assets_permanently_deleted_count": "{count, plural, one {# item} other {# items}} permanent verwijderd", "assets_removed_count": "{count, plural, one {# item} other {# items}} verwijderd", - "assets_removed_permanently_from_device": "{count} item(s) permanent verwijderd van je apparaat", + "assets_removed_permanently_from_device": "{count, plural, one {# item} other {# items}} permanent verwijderd van je apparaat", "assets_restore_confirmation": "Weet je zeker dat je alle verwijderde items wilt herstellen? Je kunt deze actie niet ongedaan maken! Offline items kunnen op deze manier niet worden hersteld.", "assets_restored_count": "{count, plural, one {# item} other {# items}} hersteld", - "assets_restored_successfully": "{count} item(s) succesvol hersteld", - "assets_trashed": "{count} item(s) naar de prullenbak verplaatst", + "assets_restored_successfully": "{count, plural, one {# item} other {# items}} succesvol hersteld", + "assets_trashed": "{count, plural, one {# item} other {# items}} naar de prullenbak verplaatst", "assets_trashed_count": "{count, plural, one {# item} other {# items}} naar prullenbak verplaatst", - "assets_trashed_from_server": "{count} item(s) naar de prullenbak verplaatst op de Immich server", + "assets_trashed_from_server": "{count, plural, one {# item} other {# items}} naar de prullenbak verplaatst op de Immich server", "assets_were_part_of_album_count": "{count, plural, one {Item was} other {Items waren}} al onderdeel van het album", "assets_were_part_of_albums_count": "{count, plural, one {Item is} other {Items zijn}} al onderdeel van de albums", "authorized_devices": "Geautoriseerde apparaten", @@ -876,7 +876,7 @@ "current_server_address": "Huidig serveradres", "custom_date": "Aangepaste datum", "custom_locale": "Aangepaste landinstelling", - "custom_locale_description": "Formatteer datums, tijden en getallen op basis van de geselecteerde taal en de regio", + "custom_locale_description": "Formatteer datums, tijden, en getallen op basis van de geselecteerde taal en regio", "custom_url": "Aangepaste URL", "cutoff_date_description": "Bewaar foto's van de laatste…", "cutoff_day": "{count, plural, one {dag} other {dagen}}", @@ -896,10 +896,10 @@ "days": "Dagen", "deduplicate_all": "Alles dedupliceren", "default_locale": "Standaard landinstelling", - "default_locale_description": "Formatteer datums en getallen op basis van de taalinstellingen van uw browser", + "default_locale_description": "Formatteer datums en getallen op basis van de taalinstellingen van je browser", "delete": "Verwijderen", "delete_action_confirmation_message": "Weet je zeker dat je dit item wilt verwijderen? Deze actie zorgt ervoor dat het item naar de prullenbak van de server wordt verplaatst en je wordt gevraagd of je deze ook lokaal wilt verwijderen", - "delete_action_prompt": "{count} item(s) verwijderd", + "delete_action_prompt": "{count} verwijderd", "delete_album": "Album verwijderen", "delete_api_key_prompt": "Weet je zeker dat je deze API-sleutel wilt verwijderen?", "delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat", @@ -913,12 +913,12 @@ "delete_key": "Verwijder key", "delete_library": "Verwijder bibliotheek", "delete_link": "Verwijder link", - "delete_local_action_prompt": "{count} item(s) lokaal verwijderd", + "delete_local_action_prompt": "{count} lokaal verwijderd", "delete_local_dialog_ok_backed_up_only": "Verwijder alleen met back-up", "delete_local_dialog_ok_force": "Toch verwijderen", "delete_others": "Andere verwijderen", "delete_permanently": "Permanent verwijderen", - "delete_permanently_action_prompt": "{count} item(s) permanent verwijderd", + "delete_permanently_action_prompt": "{count} permanent verwijderd", "delete_shared_link": "Verwijder gedeelde link", "delete_shared_link_dialog_title": "Verwijder gedeelde link", "delete_tag": "Tag verwijderen", @@ -948,7 +948,7 @@ "documentation": "Documentatie", "done": "Klaar", "download": "Downloaden", - "download_action_prompt": "{count} item(s) aan het downloaden", + "download_action_prompt": "{count, plural, one {# item} other {# items}} aan het downloaden", "download_canceled": "Download geannuleerd", "download_complete": "Download voltooid", "download_enqueue": "Download in wachtrij", @@ -980,7 +980,7 @@ "edit_birthday": "Wijzig verjaardag", "edit_date": "Datum bewerken", "edit_date_and_time": "Datum en tijd bewerken", - "edit_date_and_time_action_prompt": "Datum en tijd bijgewerkt van {count} item(s)", + "edit_date_and_time_action_prompt": "Datum en tijd bijgewerkt van {count, plural, one {# item} other {# items}}", "edit_date_and_time_by_offset": "Wijzigen datum door verschuiving", "edit_date_and_time_by_offset_interval": "Nieuw datuminterval: {from}-{to}", "edit_description": "Beschrijving bewerken", @@ -990,7 +990,7 @@ "edit_key": "Key bewerken", "edit_link": "Link bewerken", "edit_location": "Locatie bewerken", - "edit_location_action_prompt": "Locatie bijgewerkt van {count} item(s)", + "edit_location_action_prompt": "Locatie bijgewerkt van {count, plural, one {# item} other {# items}}", "edit_location_dialog_title": "Locatie", "edit_name": "Naam bewerken", "edit_people": "Mensen bewerken", @@ -1203,7 +1203,7 @@ "failed_to_load_assets": "Kan items niet laden", "failed_to_load_folder": "Laden van map mislukt", "favorite": "Favoriet", - "favorite_action_prompt": "{count} item(s) toegevoegd aan je favorieten", + "favorite_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan je favorieten", "favorite_or_unfavorite_photo": "Foto markeren als of verwijderen uit favorieten", "favorites": "Favorieten", "favorites_page_no_favorites": "Geen favoriete items gevonden", @@ -1389,7 +1389,7 @@ "library_page_sort_title": "Albumtitel", "licenses": "Licenties", "light": "Licht", - "light_theme": "Wissel naar lichte thema", + "light_theme": "Wissel naar licht thema", "like": "Vind ik leuk", "like_deleted": "Like verwijderd", "link_motion_video": "Koppel bewegende video", @@ -1551,7 +1551,7 @@ "move_off_locked_folder": "Verplaats uit vergrendelde map", "move_to": "Verplaatsen naar", "move_to_device_trash": "Naar prullenbak van apparaat", - "move_to_lock_folder_action_prompt": "{count} item(s) toegevoegd aan de vergrendelde map", + "move_to_lock_folder_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan de vergrendelde map", "move_to_locked_folder": "Verplaats naar vergrendelde map", "move_to_locked_folder_confirmation": "Deze foto’s en video’s worden uit alle albums verwijderd en zijn alleen te bekijken in de vergrendelde map", "move_up": "Naar boven verplaatsen", @@ -1854,9 +1854,9 @@ "remove_custom_date_range": "Aangepast datumbereik verwijderen", "remove_deleted_assets": "Verwijder offline bestanden", "remove_from_album": "Verwijderen uit album", - "remove_from_album_action_prompt": "{count} item(s) verwijderd uit het album", + "remove_from_album_action_prompt": "{count, plural, one {# item} other {# items}} verwijderd uit het album", "remove_from_favorites": "Verwijderen uit favorieten", - "remove_from_lock_folder_action_prompt": "{count} item(s) verwijderd uit de vergrendelde map", + "remove_from_lock_folder_action_prompt": "{count, plural, one {# item} other {# items}} verwijderd uit de vergrendelde map", "remove_from_locked_folder": "Verwijder uit de vergrendelde map", "remove_from_locked_folder_confirmation": "Weet je zeker dat je deze foto's en video's uit de vergrendelde map wilt verplaatsen? Ze zijn dan weer zichtbaar in je bibliotheek.", "remove_from_shared_link": "Verwijderen uit gedeelde link", @@ -1899,7 +1899,7 @@ "resolved_all_duplicates": "Alle duplicaten opgelost", "restore": "Herstellen", "restore_all": "Herstel alle", - "restore_trash_action_prompt": "{count} item(s) teruggehaald uit de prullenbak", + "restore_trash_action_prompt": "{count, plural, one {# item} other {# items}} teruggehaald uit de prullenbak", "restore_user": "Gebruiker herstellen", "restored_asset": "Item hersteld", "resume": "Hervatten", @@ -2067,9 +2067,9 @@ "settings_saved": "Instellingen opgeslagen", "setup_pin_code": "Stel een pincode in", "share": "Delen", - "share_action_prompt": "{count} item(s) gedeeld", + "share_action_prompt": "{count, plural, one {# item} other {# items}} gedeeld", "share_add_photos": "Foto's toevoegen", - "share_assets_selected": "{count} item(s) geselecteerd", + "share_assets_selected": "{count, plural, one {# item} other {# items}} geselecteerd", "share_dialog_preparing": "Voorbereiden...", "share_link": "Link delen", "shared": "Gedeeld", @@ -2177,7 +2177,7 @@ "sort_title": "Titel", "source": "Bron", "stack": "Stapel", - "stack_action_prompt": "{count} item(s) gestapeld", + "stack_action_prompt": "{count} items gestapeld", "stack_duplicates": "Stapel duplicaten", "stack_select_one_photo": "Selecteer één primaire foto voor de stapel", "stack_selected_photos": "Geselecteerde foto's stapelen", @@ -2264,7 +2264,7 @@ "total": "Totaal", "total_usage": "Totaal gebruik", "trash": "Prullenbak", - "trash_action_prompt": "{count} item(s) verplaatst naar de prullenbak", + "trash_action_prompt": "{count, plural, one {# item} other {# items}} verplaatst naar de prullenbak", "trash_all": "Verplaats alle naar prullenbak", "trash_count": "{count, number} naar prullenbak", "trash_delete_asset": "Items naar prullenbak verplaatsen of verwijderen", @@ -2314,7 +2314,7 @@ "unselect_all_duplicates": "Deselecteer alle duplicaten", "unselect_all_in": "Deselecteer alles in {group}", "unstack": "Ontstapelen", - "unstack_action_prompt": "{count} item(s) ontstapeld", + "unstack_action_prompt": "{count} items ontstapeld", "unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld", "unsupported_field_type": "Veldtype niet ondersteund", "unsupported_file_type": "Bestand {file} kan niet worden geüpload omdat het bestandstype {type} niet wordt ondersteund.", diff --git a/i18n/package.json b/i18n/package.json index a573aaff56..2b9548ed8b 100644 --- a/i18n/package.json +++ b/i18n/package.json @@ -1,6 +1,6 @@ { "name": "immich-i18n", - "version": "2.7.4", + "version": "2.7.5", "private": true, "scripts": { "format": "prettier --cache --check .", diff --git a/i18n/pl.json b/i18n/pl.json index 0f45c82e4a..98cd5296bc 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -1009,8 +1009,8 @@ "editor_edits_applied_success": "Zmiany zostały pomyślnie zastosowane", "editor_flip_horizontal": "Odwróć poziomo", "editor_flip_vertical": "Odwróć pionowo", - "editor_handle_corner": "{corner, select, top_left {Top-left} top_right {Top-right} bottom_left {Bottom-left} bottom_right {Bottom-right} other {A}} uchwyt narożny", - "editor_handle_edge": "{edge, select, top {Top} bottom {Bottom} left {Left} right {Right} other {An}} uchwyt krawędziowy", + "editor_handle_corner": "{corner, select, top_left {Górny lewy} top_right {Górny prawy} bottom_left {Dolny lewy} bottom_right {Dolny prawy} other {Jakiś}} uchwyt narożny", + "editor_handle_edge": "{edge, select, top {Górny} bottom {Dolny} left {Lewy} right {Prawy} other {Jakiś}} uchwyt krawędziowy", "editor_orientation": "Orientacja", "editor_reset_all_changes": "Zresetuj zmiany", "editor_rotate_left": "Obróć o 90° przeciwnie do ruchu wskazówek zegara", diff --git a/i18n/th.json b/i18n/th.json index d8d34d024a..f0f70638fd 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -5,6 +5,7 @@ "acknowledge": "รับทราบ", "action": "ดำเนินการ", "action_common_update": "อัปเดต", + "action_description": "ชุดการดำเนินการที่จะปฏิบัติกับรายการที่ผ่านการกรอง", "actions": "การดำเนินการ", "active": "กำลังทำงาน", "active_count": "กำลังทำงาน: {count}", @@ -16,11 +17,13 @@ "add_a_name": "เพิ่มชื่อ", "add_a_title": "เพิ่มหัวข้อ", "add_action": "เพิ่มการดำเนินการ", + "add_action_description": "คลิกเพื่อเพิ่มการดำเนินการ", "add_assets": "เพิ่มสื่อ", "add_birthday": "เพิ่มวันเกิด", "add_endpoint": "เพิ่มปลายทาง", "add_exclusion_pattern": "เพิ่มข้อยกเว้น", "add_filter": "เพิ่มตัวกรอง", + "add_filter_description": "คลิกเพื่อเพิ่มการกรอง", "add_location": "เพิ่มตำแหน่ง", "add_more_users": "เพิ่มผู้ใช้งาน", "add_partner": "เพิ่มคู่หู", @@ -32,12 +35,14 @@ "add_to_album_bottom_sheet_added": "เพิ่มไปยัง {album} แล้ว", "add_to_album_bottom_sheet_already_exists": "อยู่ใน {album} อยู่แล้ว", "add_to_album_bottom_sheet_some_local_assets": "ไฟล์บางส่วนไม่สามารถเพิ่มไปยังอัลบั้มได้", + "add_to_album_toggle": "สลับการเลือกสำหรับ {album}", "add_to_albums": "เพิ่มเข้าในอัลบั้ม", "add_to_albums_count": "เพิ่มไปยังอัลบั้ม ({count})", "add_to_bottom_bar": "เพิ่มไปยัง", "add_to_shared_album": "เพิ่มไปยังอัลบั้มที่แชร์", "add_upload_to_stack": "เพิ่มที่อัปโหลดเข้า stack", "add_url": "เพิ่ม URL", + "add_workflow_step": "เพิ่มขั้นตอนการทำงาน", "added_to_archive": "เพิ่มไปยังที่จัดเก็บถาวร", "added_to_favorites": "เพิ่มเข้ารายการโปรดแล้ว", "added_to_favorites_count": "เพิ่ม {count, number} รูปเข้ารายการโปรดแล้ว", @@ -70,6 +75,7 @@ "confirm_reprocess_all_faces": "คุณแน่ใจว่าคุณต้องการประมวลผลใบหน้าทั้งหมดใหม่? ชื่อคนจะถูกลบไปด้วย", "confirm_user_password_reset": "คุณแน่ใจว่าต้องการรีเซ็ตรหัสผ่านของ {user} หรือไม่?", "confirm_user_pin_code_reset": "คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตรหัส PIN ของ {user}", + "copy_config_to_clipboard_description": "คัดลอกการตั้งค่าระบบปัจจุบันในรูปแบบ JSON ไปยังคลิปบอร์ด", "create_job": "สร้างงาน", "cron_expression": "รูปแบบ cron", "cron_expression_description": "ตั้งช่วงเวลาในการสแกนโดยใช้รูปแบบ cron สำหรับข้อมูลเพิ่มเติมกรุณาอิง Crontab Guru", @@ -77,6 +83,7 @@ "disable_login": "ปิดการล็อกอิน", "duplicate_detection_job_description": "ใช้ machine learning กับสี่อเพื่อตรวจจับรูปภาพที่คล้ายกัน โดยใช้การค้นหาอัจฉริยะ", "exclusion_pattern_description": "ข้อยกเว้นสามารถละเว้นไฟล์และโฟลเดอร์ขณะสแกนคลังภาพของคุณ มีประโยชน์เมื่อโฟลเดอร์มีไฟล์ที่ไม่อยากนำเข้า เช่นไฟล์ RAW", + "export_config_as_json_description": "ดาวน์โหลดการตั้งค่าระบบปัจจุบันไปยังไฟล์ในรูปแบบ JSON", "external_libraries_page_description": "หน้าต่างคลังแอดมินภายนอก", "face_detection": "การตรวจจับใบหน้า", "face_detection_description": "ตรวจจับใบหน้าในสี่อโดยใช้ machine learning วิดีโอจะใช้ภาพตัวอย่างจากวิดีโอเท่านั้น \"ทั้งหมด\" จะประมวลผลสี่อทั้งหมด \"ขาดหาย\" จะประมวลผลสี่อที่ยังไม่ได้ประมวลผล ใบหน้าที่ถูกตรวจจับแล้วจะถูกเข้าคิวประมวลผลการจดจำใบหน้า เพิ่มเข้าไปในกลุ่มที่มีอยู่แล้วหรือคนใหม่", @@ -97,6 +104,8 @@ "image_preview_description": "ภาพขนาดปานกลางที่ถูกลบข้อมูลเมตา ใช้สำหรับการดูแอสเซ็ตเดี่ยวและสำหรับการเรียนรู้ของเครื่อง (Machine Learning)", "image_preview_quality_description": "คุณภาพการแสดงตัวอย่างตั้งแต่ 1-100 ยิ่งสูงยิ่งดี แต่จะทำให้ไฟล์มีขนาดใหญ่ขึ้นและอาจทำให้แอปตอบสนองช้าลง การตั้งค่าต่ำอาจส่งผลต่อคุณภาพ Machine Learning", "image_preview_title": "ตั้งค่าพรีวิว", + "image_progressive": "รูปภาพแบบโปรเกรสซีฟ", + "image_progressive_description": "เข้ารหัสรูปภาพ JPEG แบบโปรเกรสซีฟเพื่อให้แสดงผลแบบค่อยๆ ชัดขึ้นขณะโหลด ทั้งนี้จะไม่มีผลกับรูปภาพ WebP", "image_quality": "คุณภาพ", "image_resolution": "ความละเอียด", "image_resolution_description": "ความละเอียดสูกว่าสามารถเก็บรายละเอียดได้มากกว่าแต่ใช้เวลา encode นานกว่า ไฟล์ใหญ่กว่า และลดความตอบสนองของแอป", @@ -105,6 +114,7 @@ "image_thumbnail_description": "รูปขนาดย่อที่มีการลบข้อมูลเมตาด้าต้า ใช้เมื่อดูภาพถ่ายในกลุ่ม เช่น ในไทม์ไลน์หลัก", "image_thumbnail_quality_description": "คุณภาพของภาพขนาดย่อตั้งแต่ 1-100 ยิ่งสูงยิ่งดี แต่จะทำให้ไฟล์มีขนาดใหญ่ขึ้นและอาจทำให้แอปตอบสนองช้าลง", "image_thumbnail_title": "ตั้งค่า Thumbnail", + "import_config_from_json_description": "นำเข้าการตั้งค่าระบบโดยการอัปโหลดไฟล์คอนฟิก JSON", "job_concurrency": "{job} งานพร้อมกัน", "job_created": "สร้างงานเรียบร้อย", "job_not_concurrency_safe": "งานนี้ทำงานพร้อมกันแบบปลอดภัยไม่ได้", @@ -510,10 +520,10 @@ "always_keep_photos_hint": "\"เพิ่มพื้นที่ว่าง\" จะเก็บรูปภาพทั้งหมดบนอุปกรณ์นี้", "always_keep_videos_hint": "\"เพิ่มพื้นที่ว่าง\" จะเก็บวิดีโอทั้งหมดบนอุปกรณ์นี้", "anti_clockwise": "ทวนเข็มนาฬิกา", - "api_key": "API key", + "api_key": "คีย์ API", "api_key_description": "ค่านี้จะแสดงเพียงครั้งเดียว โปรดคัดลอกก่อนปิดหน้าต่าง", - "api_key_empty": "ชื่อ API Key ของคุณไม่ควรว่างเปล่า", - "api_keys": "API Key", + "api_key_empty": "ชื่อคีย์ API ของคุณไม่ควรว่างเปล่า", + "api_keys": "คีย์ API", "app_architecture_variant": "รูปแบบ (สถาปัตยกรรม)", "app_bar_signout_dialog_content": "คุณแน่ใจว่าอยากออกจากระบบ", "app_bar_signout_dialog_ok": "ใช่", @@ -867,7 +877,7 @@ "delete": "ลบออก", "delete_action_prompt": "ลบ {count} รายการแล้ว", "delete_album": "ลบอัลบั้ม", - "delete_api_key_prompt": "คุณต้องการลบ API คีย์ นี้ใช่ไหม ?", + "delete_api_key_prompt": "คุณต้องการลบคีย์ API นี้หรือไม่?", "delete_dialog_alert": "รายการดังกล่าวจะถูกลบจาก Immich และเครื่องอย่างถาวร", "delete_dialog_alert_local": "รายการดังกล่าวจะถูกลบจากเครื่องคุณอย่างถาวร แต่จะยังคงอยู่บนเซิร์ฟเวอร์ Immich", "delete_dialog_alert_local_non_backed_up": "รายการบางตัวไม่ได้ถูกสำรองบน Immich และจะถูกลบจากเครื่องคุณอย่างถาวร", @@ -1062,7 +1072,7 @@ "unable_to_connect": "ไม่สามารถเชื่อมต่อได้", "unable_to_copy_to_clipboard": "ไม่สามารถคัดลอกไปยังคลิปบอร์ดได้ ตรวจสอบให้แน่ใจว่าคุณเข้าถึงหน้าผ่านทาง https", "unable_to_create_admin_account": "ไม่สามารถสร้างบัญชีผู้ดูแลระบบได้", - "unable_to_create_api_key": "ไม่สามารถสร้าง API คีย์ ได้", + "unable_to_create_api_key": "ไม่สามารถสร้างคีย์ API", "unable_to_create_library": "ไม่สามารถสร้างคลังภาพได้", "unable_to_create_user": "ไม่สามารถสร้างผู้ใช้ได้", "unable_to_delete_album": "ไม่สามารถลบอัลบั้มได้", @@ -1089,7 +1099,7 @@ "unable_to_reassign_assets_new_person": "ไม่สามารถมอบหมาย ให้กับบุคคลใหม่ได้", "unable_to_refresh_user": "ไม่สามารถรีเฟรชผู้ใช้ได้", "unable_to_remove_album_users": "ไม่สามารถลบผู้ใช้ออกจากอัลบั้มได้", - "unable_to_remove_api_key": "ไม่สามารถลบ API Key ได้", + "unable_to_remove_api_key": "ไม่สามารถลบคีย์ API", "unable_to_remove_assets_from_shared_link": "ไม่สามารถลบออกจากลิงก์ที่แชร์ได้", "unable_to_remove_library": "ไม่สามารถลบคลังภาพได้", "unable_to_remove_partner": "ไม่สามารถลบคู่หูได้", @@ -1101,7 +1111,7 @@ "unable_to_restore_trash": "ไม่สามารถเรียกคืนถังขยะได้", "unable_to_restore_user": "ไม่สามารถเรียกคืนผู้ใช้ได้", "unable_to_save_album": "ไม่สามารถบันทึกอัลบั้มได้", - "unable_to_save_api_key": "ไม่สามารถบันทึก API คีย์ ได้", + "unable_to_save_api_key": "ไม่สามารถบันทึกคีย์ API", "unable_to_save_date_of_birth": "ไม่สามารถบันทึกวันเกิดได้", "unable_to_save_name": "ไม่สามารถบันทึกชื่อได้", "unable_to_save_profile": "ไม่สามารถบันทึกโปรไฟล์ได้", @@ -1195,7 +1205,7 @@ "geolocation_instruction_location": "คลิกบนสื่อที่มีพิกัด GPS เพื่อใช้ตำแหน่งนั้น หรือเลือกตำแหน่งจากแผนที่โดยตรง", "get_help": "ขอความช่วยเหลือ", "get_people_error": "ข้อผิดพลาดขณะดึงข้อมูลผู้คน", - "get_wifiname_error": "ไม่สามารถรับชื่อ Wi-Fi กรุณายืนยันการให้อนุญาตแอพ และยืนยันว่า Wi-Fi เชื่อมต่ออยู่", + "get_wifiname_error": "ไม่สามารถรับชื่อ Wi-Fi กรุณายืนยันการให้อนุญาตแอป และยืนยันว่าเชื่อมต่อกับเครือข่าย Wi-Fi อยู่", "getting_started": "เริ่มต้นใช้งาน", "go_back": "กลับ", "go_to_folder": "ไปที่โฟล์เดอร์", @@ -1430,7 +1440,7 @@ "manage_sharing_with_partners": "จัดการการแชร์กับคู่หู", "manage_the_app_settings": "จัดการการตั้งค่าแอป", "manage_your_account": "จัดการบัญชีของคุณ", - "manage_your_api_keys": "จัดการกุญแจ API ของคุณ", + "manage_your_api_keys": "จัดการคีย์ API ของคุณ", "manage_your_devices": "จัดการอุปกรณ์ของคุณ", "manage_your_oauth_connection": "จัดการการเชื่อมต่อ OAuth ของคุณ", "map": "แผนที่", @@ -1516,7 +1526,7 @@ "networking_subtitle": "ตั้งค่าปลายทางเซิร์ฟเวอร์", "never": "ไม่เคย", "new_album": "อัลบั้มใหม่", - "new_api_key": "สร้าง API คีย์ใหม่", + "new_api_key": "สร้างคีย์ API ใหม่", "new_date_range": "ช่วงวันที่ใหม่", "new_password": "รหัสผ่านใหม่", "new_person": "คนใหม่", @@ -1581,7 +1591,7 @@ "on_this_device": "บนอุปกรณ์นี้", "onboarding": "การเริ่มต้นใช้งาน", "onboarding_locale_description": "เลือกภาษาที่คุณต้องการ คุณสามารถเปลี่ยนได้ภายหลังในการตั้งค่า", - "onboarding_privacy_description": "ฟีเจอร์ (ตัวเลือก) ต่อไปนี้ต้องอาศัยบริการภายนอก และสามารถปิดใช้งานได้ตลอดเวลาในการตั้งค่าการ", + "onboarding_privacy_description": "คุณสมบัติ (ตัวเลือก) ต่อไปนี้ต้องอาศัยบริการภายนอก และสามารถปิดใช้งานได้ตลอดเวลาในการตั้งค่า", "onboarding_server_welcome_description": "มาตั้งค่าเซิร์ฟเวอร์ของคุณด้วยการตั้งค่าที่ใช้บ่อยกันเถอะ", "onboarding_theme_description": "เลือกธีมสี คุณสามารถเปลี่ยนแปลงได้ในภายหลังในการตั้งค่าของคุณ", "onboarding_user_welcome_description": "มาเริ่มต้นกันเถอะ!", @@ -1631,7 +1641,7 @@ "pattern": "รูปแบบ", "pause": "หยุด", "pause_memories": "หยุดดูความทรงจํา", - "paused": "หยุด", + "paused": "หยุดชั่วคราว", "pending": "กำลังรอ", "people": "ผู้คน", "people_edits_count": "{count, plural, one {# person} other {# people}} ถูกแก้ไข", @@ -1644,11 +1654,13 @@ "permanently_delete_assets_prompt": "คุณแน่ใจหรือไม่ว่าต้องการลบ {count, plural, one {this asset?} other {these # asset?}}อย่างถาวร การดำเนินการนี้จะลบ {count, plural, one {it from its} other {them from their}} อัลบั้มด้วย", "permanently_deleted_asset": "ลบสื่อถาวรแล้ว", "permanently_deleted_assets_count": "ลบ {count, plural, one {# asset} other {# assets}} เรียบร้อยแล้ว", + "permission": "สิทธิ์", + "permission_empty": "สิทธิ์ของคุณต้องไม่เว้นว่าง", "permission_onboarding_back": "กลับ", "permission_onboarding_continue_anyway": "ดำเนินการต่อ", "permission_onboarding_get_started": "เริ่มต้น", "permission_onboarding_go_to_settings": "ไปยังการตั้งค่า", - "permission_onboarding_permission_denied": "ไม่อนุญาต ตั้งค่าสิทธิ์เข้าถึงรูปภาพและวิดีโอเพื่อใช้งาน Immich", + "permission_onboarding_permission_denied": "สิทธิ์ถูกปฏิเสธ กรุณาให้สิทธิ์เข้าถึงรูปภาพและวิดีโอเพื่อใช้งาน Immich", "permission_onboarding_permission_granted": "ให้สิทธิ์สำเร็จ คุณพร้อมใช้งานแล้ว", "permission_onboarding_permission_limited": "สิทธ์จำกัด เพื่อให้ Immich สำรองข้อมูลและจัดการคลังภาพได้ ตั้งค่าสิทธิเข้าถึงรูปภาพและวิดีโอ", "permission_onboarding_request": "Immich จำเป็นจะต้องได้รับสิทธิ์ดูรูปภาพและวิดีโอ", @@ -1783,7 +1795,7 @@ "remove_photo_from_memory": "ลบรูปออกจากความทรงจำนี้", "remove_url": "ลบ URL", "remove_user": "ลบผู้ใช้", - "removed_api_key": "API คีย์ของ: {name} ถูกลบแล้ว", + "removed_api_key": "ลบคีย์ API แล้ว: {name}", "removed_from_archive": "ลบจากเก็บถาวรแล้ว", "removed_from_favorites": "ลบจากรายการโปรดแล้ว", "removed_from_favorites_count": "{count, plural, other {ถูกลบ#}} จากรายการโปรดแล้ว", @@ -1831,7 +1843,7 @@ "save": "บันทึก", "save_to_gallery": "บันทึกไปยังแกลเลอรี", "saved": "บันทึกแล้ว", - "saved_api_key": "บันทึก API คีย์ แล้ว", + "saved_api_key": "บันทึกคีย์ API แล้ว", "saved_profile": "แก้ไขโปรไฟล์สำเร็จ", "saved_settings": "บันทึกการตั้งค่าสำเร็จ", "say_something": "พูดอะไรสักอย่าง", @@ -2226,7 +2238,7 @@ "upload_status_duplicates": "รายการซ้ำ", "upload_status_errors": "ข้อผิดพลาด", "upload_status_uploaded": "อัปโหลดแล้ว", - "upload_success": "อัปโหลดสำเร็จ รีเฟรชหน้าเพื่อดูสื่อใหม่ที่อัปโหลดล่าสุด", + "upload_success": "อัปโหลดสำเร็จ รีเฟรชหน้าเพื่อดูสื่อที่อัปโหลดใหม่", "upload_to_immich": "อัปโหลดไปยัง Immich ({count})", "uploading": "กำลังอัปโหลด", "uploading_media": "กำลังอัปโหลดสื่อ", diff --git a/i18n/uk.json b/i18n/uk.json index c9ff3d568a..b2b0a01501 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -849,9 +849,12 @@ "create_link_to_share": "Створити посилання спільного доступу", "create_link_to_share_description": "Дати змогу будь-кому переглядати вибрані фото за посиланням", "create_new": "СТВОРИТИ НОВИЙ", + "create_new_face": "Створити нове обличчя", "create_new_person": "Створити нову людину", "create_new_person_hint": "Призначити вибрані елементи новій людині", "create_new_user": "Створити нового користувача", + "create_person": "Створити людину", + "create_person_subtitle": "Додайте ім'я до вибраного обличчя, щоб створити та позначити нову особу", "create_shared_album_page_share_add_assets": "ДОДАТИ ЕЛЕМЕНТИ", "create_shared_album_page_share_select_photos": "Вибрати фото", "create_shared_link": "Створити спільне посилання", @@ -2214,6 +2217,7 @@ "tag": "Тег", "tag_assets": "Додати теги", "tag_created": "Створено тег: {tag}", + "tag_face": "Тег обличчя", "tag_feature_description": "Перегляд фото та відео, згрупованих за логічними темами тегів", "tag_not_found_question": "Не вдається знайти тег? Створіть новий тег.", "tag_people": "Позначити людей", diff --git a/i18n/vi.json b/i18n/vi.json index fa37d05fb2..5c3ace5da0 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -537,10 +537,10 @@ "app_bar_signout_dialog_content": "Bạn có muốn đăng xuất?", "app_bar_signout_dialog_ok": "Có", "app_bar_signout_dialog_title": "Đăng xuất", - "app_download_links": "Liên kết tải app", - "app_settings": "App", - "app_stores": "Cửa hàng app", - "app_update_available": "Đã có bản cập nhật app", + "app_download_links": "Liên kết tải ứng dụng", + "app_settings": "Ứng dụng", + "app_stores": "Cửa hàng ứng dụng", + "app_update_available": "Đã có bản cập nhật ứng dụng", "appears_in": "Xuất hiện trong", "apply_count": "Áp dụng ({count, number})", "archive": "Lưu trữ", @@ -617,7 +617,7 @@ "back_close_deselect": "Quay lại, đóng, hoặc bỏ chọn", "background_backup_running_error": "Sao lưu nền hiện đang chạy, không thể bắt đầu sao lưu thủ công", "background_location_permission": "Quyền truy cập vị trí khi chạy nền", - "background_location_permission_content": "Để chuyển đổi mạng khi chạy ở chế độ nền, Immich *luôn* phải có quyền truy cập vị trí chính xác để có thể đọc tên mạng Wi-Fi", + "background_location_permission_content": "Để chuyển đổi mạng khi chạy ở chế độ nền, Immich phải *luôn* có quyền truy cập vị trí chính xác để có thể đọc tên mạng Wi-Fi", "background_options": "Tùy chọn nền", "backup": "Sao lưu", "backup_album_selection_page_albums_device": "Album trên thiết bị ({count})", @@ -637,8 +637,8 @@ "backup_background_service_in_progress_notification": "Đang sao lưu tệp của bạn…", "backup_background_service_upload_failure_notification": "Tải lên {filename} thất bại", "backup_controller_page_albums": "Album sao lưu", - "backup_controller_page_background_app_refresh_disabled_content": "Bật làm mới app trong nền tại Cài đặt > Cài đặt chung > Làm mới app trong nền để dùng sao lưu nền.", - "backup_controller_page_background_app_refresh_disabled_title": "Làm mới app trong nền bị vô hiệu hoá", + "backup_controller_page_background_app_refresh_disabled_content": "Bật làm mới ứng dụng trong nền tại Cài đặt > Cài đặt chung > Làm mới ứng dụng trong nền để dùng sao lưu nền.", + "backup_controller_page_background_app_refresh_disabled_title": "Làm mới ứng dụng trong nền bị vô hiệu hoá", "backup_controller_page_background_app_refresh_enable_button_text": "Đi tới cài đặt", "backup_controller_page_background_battery_info_link": "Hướng dẫn tôi", "backup_controller_page_background_battery_info_message": "Để có trải nghiệm sao lưu nền tốt nhất, vui lòng vô hiệu hóa bất kỳ tối ưu hóa pin nào đang hạn chế hoạt động nền của Immich.\n\nVì điều này phụ thuộc vào thiết bị, vui lòng tham khảo thông tin cần thiết của nhà sản xuất thiết bị của bạn.", @@ -647,7 +647,7 @@ "backup_controller_page_background_charging": "Chỉ khi đang sạc", "backup_controller_page_background_configure_error": "Cấu hình dịch vụ nền thất bại", "backup_controller_page_background_delay": "Trì hoãn sao lưu tệp mới: {duration}", - "backup_controller_page_background_description": "Bật dịch vụ nền để tự động sao lưu tệp mới mà không cần mở app", + "backup_controller_page_background_description": "Bật dịch vụ nền để tự động sao lưu tệp mới mà không cần mở ứng dụng", "backup_controller_page_background_is_off": "Sao lưu tự động trong nền đang tắt", "backup_controller_page_background_is_on": "Sao lưu tự động trong nền đang bật", "backup_controller_page_background_turn_off": "Tắt dịch vụ nền", @@ -657,7 +657,7 @@ "backup_controller_page_backup_selected": "Đã chọn: ", "backup_controller_page_backup_sub": "Ảnh và video đã sao lưu", "backup_controller_page_created": "Tạo vào: {date}", - "backup_controller_page_desc_backup": "Bật sao lưu khi app hoạt động để tự động sao lưu tệp mới lên máy chủ khi mở app.", + "backup_controller_page_desc_backup": "Bật sao lưu khi ứng dụng hoạt động để tự động sao lưu tệp mới lên máy chủ khi mở ứng dụng.", "backup_controller_page_excluded": "Đã bỏ qua: ", "backup_controller_page_failed": "Thất bại ({count})", "backup_controller_page_filename": "Tên tệp: {filename} [{size}]", @@ -668,12 +668,12 @@ "backup_controller_page_remainder_sub": "Số lượng ảnh và video đã chọn chưa được sao lưu", "backup_controller_page_server_storage": "Dung lượng máy chủ", "backup_controller_page_start_backup": "Bắt đầu sao lưu", - "backup_controller_page_status_off": "Sao lưu tự động khi app hoạt động đang tắt", - "backup_controller_page_status_on": "Sao lưu tự động khi app hoạt động đang bật", + "backup_controller_page_status_off": "Sao lưu tự động khi ứng dụng hoạt động đang tắt", + "backup_controller_page_status_on": "Sao lưu tự động khi ứng dụng hoạt động đang bật", "backup_controller_page_storage_format": "Đã dùng {used} của {total}", "backup_controller_page_to_backup": "Các album cần được sao lưu", "backup_controller_page_total_sub": "Tất cả ảnh và video không trùng lập từ các album được chọn", - "backup_controller_page_turn_off": "Tắt sao lưu khi app hoạt động", + "backup_controller_page_turn_off": "Tắt sao lưu khi ứng dụng hoạt động", "backup_controller_page_turn_on": "Bật sao lưu khi mở app", "backup_controller_page_uploading_file_info": "Thông tin tệp đang tải lên", "backup_err_only_album": "Không thể xóa album duy nhất", @@ -704,16 +704,16 @@ "bulk_trash_duplicates_confirmation": "Bạn có chắc muốn đưa {count, plural, one {# tệp trùng lặp} other {# tệp trùng lặp}} vào thùng rác? Điều này sẽ giữ lại ảnh chất lượng nhất của mỗi nhóm và đưa tất cả các bản trùng lặp khác vào thùng rác.", "buy": "Mua Immich", "cache_settings_clear_cache_button": "Xóa bộ nhớ đệm", - "cache_settings_clear_cache_button_title": "Xóa bộ nhớ đệm của app. Điều này sẽ ảnh hưởng đến hiệu suất của app đến khi bộ nhớ đệm được tạo lại.", + "cache_settings_clear_cache_button_title": "Xóa bộ nhớ đệm của ứng dụng. Điều này sẽ ảnh hưởng đến hiệu suất của ứng dụng đến khi bộ nhớ đệm được tạo lại.", "cache_settings_duplicated_assets_clear_button": "XÓA", - "cache_settings_duplicated_assets_subtitle": "Ảnh và video không được phép hiển thị trên app", + "cache_settings_duplicated_assets_subtitle": "Ảnh và video không được phép hiển thị trên ứng dụng", "cache_settings_duplicated_assets_title": "Tệp bị trùng ({count})", "cache_settings_statistics_album": "Ảnh thu nhỏ thư viện", "cache_settings_statistics_full": "Ảnh đầy đủ", "cache_settings_statistics_shared": "Ảnh thu nhỏ album chia sẻ", "cache_settings_statistics_thumbnail": "Ảnh thu nhỏ", "cache_settings_statistics_title": "Mức sử dụng bộ nhớ đệm", - "cache_settings_subtitle": "Kiểm soát hành vi bộ nhớ đệm của app Immich", + "cache_settings_subtitle": "Kiểm soát hành vi bộ nhớ đệm của Immich", "cache_settings_tile_subtitle": "Kiểm soát cách xử lý lưu trữ cục bộ", "cache_settings_tile_title": "Lưu trữ cục bộ", "cache_settings_title": "Cài đặt bộ nhớ đệm", @@ -999,8 +999,8 @@ "editor_discard_edits_confirm": "Bỏ thay đổi", "editor_discard_edits_prompt": "Bạn có những thay đổi chưa được lưu. Bạn có chắc chắn muốn hủy bỏ chúng không?", "editor_discard_edits_title": "Hủy thay đổi?", - "editor_edits_applied_error": "Lỗi khi áp dụng thay đổi", - "editor_edits_applied_success": "Thay đổi được áp dụng thành công", + "editor_edits_applied_error": "Không thể áp dụng chỉnh sửa", + "editor_edits_applied_success": "Chỉnh sửa được áp dụng thành công", "editor_flip_horizontal": "Lật ngang", "editor_flip_vertical": "Lật dọc", "editor_orientation": "Định hướng", @@ -1202,7 +1202,7 @@ "feature_photo_updated": "Đã cập nhật ảnh nổi bật", "features": "Tính năng", "features_in_development": "Tính năng đang được phát triển", - "features_setting_description": "Quản lý các tính năng app", + "features_setting_description": "Quản lý các tính năng ứng dụng", "file_name_or_extension": "Tên hoặc phần mở rộng tập tin", "file_name_text": "Tên tệp", "file_name_with_value": "Tên tệp: {file_name}", @@ -1280,7 +1280,7 @@ "home_page_delete_remote_err_local": "Tệp trên thiết bị trong lựa chọn xóa từ xa, bỏ qua", "home_page_favorite_err_local": "Không thể thích tệp trên thiết bị, bỏ qua", "home_page_favorite_err_partner": "Không thể thích tệp của người thân, bỏ qua", - "home_page_first_time_notice": "Nếu đây là lần đầu bạn dùng app, hãy chọn một album sao lưu để dòng thời gian có thể hiển thị ảnh và video của bạn", + "home_page_first_time_notice": "Nếu đây là lần đầu bạn dùng ứng dụng, hãy chọn một album sao lưu để dòng thời gian có thể hiển thị ảnh và video của bạn", "home_page_locked_error_local": "Không thể di chuyển tệp trên thiết bị đến thư mục Khóa, bỏ qua", "home_page_locked_error_partner": "Không thể di chuyển tệp của người thân đến thư mục Khóa, bỏ qua", "home_page_share_err_local": "Không thể chia sẻ tệp trên thiết bị qua liên kết, bỏ qua", @@ -1395,7 +1395,7 @@ "local_id": "ID cục bộ", "local_media_summary": "Mô tả phương tiện trên thiết bị", "local_network": "Mạng nội bộ", - "local_network_sheet_info": "App sẽ kết nối với máy chủ qua URL này khi sử dụng mạng Wi-Fi được chỉ định", + "local_network_sheet_info": "Ứng dụng sẽ kết nối với máy chủ qua URL này khi sử dụng mạng Wi-Fi được chỉ định", "location": "Địa điểm", "location_permission": "Quyền truy cập vị trí", "location_permission_content": "Để sử dụng tính năng tự động chuyển đổi, Immich cần có quyền vị trí chính xác để có thể đọc tên của mạng Wi-Fi hiện tại", @@ -1471,11 +1471,11 @@ "manage_geolocation": "Quản lý địa điểm", "manage_media_access_rationale": "Để có thể di chuyển tệp vào thùng rác và khôi phục chúng từ đó.", "manage_media_access_settings": "Mở cài đặt", - "manage_media_access_subtitle": "Cho phép app [Immich] quản lý và di chuyển tệp.", + "manage_media_access_subtitle": "Cho phép ứng dụng [Immich] quản lý và di chuyển tệp.", "manage_media_access_title": "Quản lý phương tiện", "manage_shared_links": "Quản lý liên kết chia sẻ", "manage_sharing_with_partners": "Quản lý chia sẻ với người thân", - "manage_the_app_settings": "Quản lý cài đặt app", + "manage_the_app_settings": "Quản lý cài đặt ứng dụng", "manage_your_account": "Quản lý tài khoản của bạn", "manage_your_api_keys": "Quản lý các khóa API của bạn", "manage_your_devices": "Quản lý các thiết bị đã đăng nhập của bạn", @@ -1490,7 +1490,7 @@ "map_marker_for_images": "Đánh dấu bản đồ cho ảnh chụp tại {city}, {country}", "map_marker_with_image": "Đánh dấu bản đồ với ảnh", "map_no_location_permission_content": "Cần quyền truy cập vị trí để hiển thị tệp từ vị trí hiện tại của bạn. Bạn có muốn cho phép ngay bây giờ không?", - "map_no_location_permission_title": "App không được phép truy cập vị trí", + "map_no_location_permission_title": "Ứng dụng không được phép truy cập vị trí", "map_settings": "Cài đặt bản đồ", "map_settings_dark_mode": "Chế độ tối", "map_settings_date_range_option_day": "Trong vòng 24 giờ qua", @@ -1746,7 +1746,7 @@ "play_transcoded_video": "Phát video đã chuyển mã", "please_auth_to_access": "Vui lòng xác thực để truy cập", "port": "Cổng", - "preferences_settings_subtitle": "Tùy chỉnh trải nghiệm app", + "preferences_settings_subtitle": "Tùy chỉnh trải nghiệm ứng dụng", "preferences_settings_title": "Cá nhân hóa", "preparing": "Đang chuẩn bị", "preset": "Mẫu có sẵn", @@ -1760,7 +1760,7 @@ "primary": "Chính", "privacy": "Bảo mật", "profile": "Hồ sơ", - "profile_drawer_app_logs": "Log", + "profile_drawer_app_logs": "Nhật ký", "profile_drawer_client_server_up_to_date": "Máy khách và máy chủ đã cập nhật", "profile_drawer_github": "GitHub", "profile_drawer_readonly_mode": "Đã bật chế độ chỉ-xem. Nhấn giữ ảnh đại diện người dùng để tắt.", @@ -2006,7 +2006,7 @@ "send_message": "Gửi tin nhắn", "send_welcome_email": "Gửi email chào mừng", "server_endpoint": "Địa chỉ máy chủ", - "server_info_box_app_version": "Phiên bản app", + "server_info_box_app_version": "Phiên bản ứng dụng", "server_info_box_server_url": "URL máy chủ", "server_offline": "Máy chủ ngoại tuyến", "server_online": "Phiên bản", @@ -2228,7 +2228,7 @@ "theme_setting_primary_color_title": "Màu chủ đạo", "theme_setting_system_primary_color_title": "Dùng màu hệ thống", "theme_setting_system_theme_switch": "Tự động (Giống thiết bị)", - "theme_setting_theme_subtitle": "Chọn cài đặt giao diện app", + "theme_setting_theme_subtitle": "Chọn cài đặt giao diện ứng dụng", "theme_setting_three_stage_loading_subtitle": "Tải ba giai đoạn có thể tăng tốc độ tải ảnh nhưng sẽ tốn dữ liệu mạng đáng kể", "theme_setting_three_stage_loading_title": "Bật tải ba giai đoạn", "then": "Tiếp theo", @@ -2276,7 +2276,7 @@ "troubleshoot": "Khắc phục sự cố", "type": "Loại", "unable_to_change_pin_code": "Thay đổi mã PIN thất bại", - "unable_to_check_version": "Không thể kiểm tra phiên bản app hoặc máy chủ", + "unable_to_check_version": "Không thể kiểm tra phiên bản ứng dụng hoặc máy chủ", "unable_to_setup_pin_code": "Thiết lập mã PIN thất bại", "unarchive": "Bỏ lưu trữ", "unarchive_action_prompt": "{count} đã bỏ khỏi Lưu trữ", diff --git a/i18n/yue_Hant.json b/i18n/yue_Hant.json index e0cf403712..19666b0af5 100644 --- a/i18n/yue_Hant.json +++ b/i18n/yue_Hant.json @@ -87,6 +87,7 @@ "external_libraries_page_description": "管理外部媒體庫嘅頁面", "face_detection": "人面偵測", "face_detection_description": "使用機器學習偵測項目中嘅臉孔。對於影片,只係會分析縮圖。「重新整理」會重新處理所有嘅項目;「重設」就會額外清除目前嘅臉孔資料;「加入排程」會將尚未處理嘅項目加入序列。完成「臉孔偵測」後,偵測到嘅臉孔將會加入「臉孔辨識」排程,並歸類到而家或者新嘅人物群組。", + "facial_recognition_job_description": "將偵測到嘅臉孔歸類為人物。此步驟會在臉孔偵測完成後執行。「重設」會重新對所有嘅臉孔進行分群;「加入排程」就會將未指派人物嘅臉孔加入序列。", "failed_job_command": "執行{job}任務嘅{command}指令失敗", "force_delete_user_warning": "警告:呢個會立即刪除用戶同埋佢所有嘅檔案。呢個係無法撤銷嘅動作,而且刪除嘅檔案將冇辦法復原。", "image_format": "格式", @@ -99,6 +100,7 @@ "image_prefer_embedded_preview": "偏向嵌入預覽", "image_prefer_embedded_preview_setting_description": "喺可用嘅時候將 RAW 相片中的內嵌預覽作為影像處理嘅輸入來源。雖然呢個設定可以令到部分相片嘅色彩更加準確,但預覽品質取決於相機,且影像可能會出現較多壓縮瑕疵。", "image_prefer_wide_gamut": "傾向廣色域", + "image_prefer_wide_gamut_setting_description": "使用 Display P3 製作縮圖:可以更好地保留廣色域影像嘅鮮豔度,但係喺舊裝置同舊版瀏覽器上,影像呈現嘅效果可能會有所唔同。sRGB 影像會保留為 sRGB,以避免色彩偏移。", "image_preview_description": "中等尺寸嘅圖片,用嚟檢視單一影像同埋機器學習", "image_preview_title": "預覽設定", "image_progressive": "逐步", diff --git a/i18n/zh_Hans.json b/i18n/zh_Hans.json index a59ec9cb9f..49dbce4035 100644 --- a/i18n/zh_Hans.json +++ b/i18n/zh_Hans.json @@ -1635,7 +1635,7 @@ "notifications": "通知", "notifications_setting_description": "管理通知", "oauth": "OAuth", - "obtainium_configurator": "Obtainium配置器", + "obtainium_configurator": "Obtainium 配置器", "obtainium_configurator_instructions": "使用 Obtainium 直接从 Immich 的 GitHub 发布页安装和更新 Android 应用。请创建一个 API 密钥并选择一个版本,以生成你的 Obtainium 配置链接", "ocr": "OCR", "official_immich_resources": "Immich官方资源", @@ -1710,7 +1710,7 @@ "permanent_deletion_warning_setting_description": "永久删除照片/视频时显示警告", "permanently_delete": "永久删除", "permanently_delete_assets_count": "永久删除{count, plural, one {个项目} other {个项目}}", - "permanently_delete_assets_prompt": "确定要永久删除 {count, plural, one {此项目吗?} other {这#个项目吗?}}这也会将{count, plural, one {其} other {它们}}从所属相簿中移除。", + "permanently_delete_assets_prompt": "确定要永久删除 {count, plural, one {此项目吗?} other {这 # 个项目吗?}}这也会将{count, plural, one {其} other {它们}}从所属相簿中移除。", "permanently_deleted_asset": "永久删除的项目", "permanently_deleted_assets_count": "已永久删除{count, plural, one {#个项目} other {#个项目}}", "permission": "权限", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index 0140fe8bb8..00b9629264 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -54,7 +54,7 @@ "authentication_settings_description": "管理密碼、OAuth 與其他驗證設定", "authentication_settings_disable_all": "您確定要停用所有登入方式嗎?這將導致完全無法登入。", "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令。", - "background_task_job": "背景工作", + "background_task_job": "背景任務", "backup_database": "建立資料庫備份", "backup_database_enable_description": "啟用資料庫備份", "backup_keep_last_amount": "保留先前備份的數量", @@ -63,7 +63,7 @@ "backup_onboarding_3_description": "您資料的總備份份數,包括原始檔案在內。這包括 1 份異地備份與 2 份本機副本。", "backup_onboarding_description": "建議採用 3-2-1 備份策略 來保護您的資料。您應保留已上傳的相片/影片副本,以及 Immich 資料庫,以建立完整的備份方案。", "backup_onboarding_footer": "更多備份 Immich 資訊,請參考 說明文件。", - "backup_onboarding_parts_title": "遵從備份原則 3-2-1:", + "backup_onboarding_parts_title": "3-2-1 備份包含:", "backup_onboarding_title": "備份", "backup_settings": "資料庫備份設定", "backup_settings_description": "管理資料庫備份設定。", @@ -81,20 +81,20 @@ "cron_expression_description": "使用 Cron 格式設定掃描間隔。更多資訊請參閱 Crontab Guru", "cron_expression_presets": "Cron 表達式預設值", "disable_login": "停用登入", - "duplicate_detection_job_description": "依靠智慧搜尋。對項目執行機器學習來偵測相似圖片", + "duplicate_detection_job_description": "針對資產執行機器學習以偵測相似圖片。需依賴「智慧搜尋」功能", "exclusion_pattern_description": "排除模式可讓您在掃描媒體庫時忽略特定檔案與資料夾。若某些資料夾包含您不想匯入的檔案(例如 RAW 檔),此功能將非常有用。", "export_config_as_json_description": "將目前系統設定下載為 JSON 檔案", "external_libraries_page_description": "管理外部媒體庫頁面", "face_detection": "臉孔偵測", - "face_detection_description": "使用機器學習偵測項目中的臉孔。對於影片,僅會分析縮圖。「重新整理」會重新處理所有項目;「重設」則會額外清除目前的臉孔資料;「加入排程」會將尚未處理的項目加入序列。完成「臉孔偵測」後,偵測到的臉孔將加入「臉孔辨識」排程,並歸類至現有或新的人物群組。", - "facial_recognition_job_description": "將偵測到的臉孔歸類為人物。此步驟會在臉孔偵測完成後執行。「重設」會重新對所有臉孔進行分群;「加入排程」則會將尚未指派人物的臉孔加入序列。", + "face_detection_description": "使用機器學習偵測項目中的臉孔。對於影片,僅會分析縮圖。「重新整理」會重新處理所有項目;「重設」則會額外清除目前的臉孔資料;「加入排程」會將尚未處理的項目加入佇列。完成「臉孔偵測」後,偵測到的臉孔將加入「臉孔辨識」排程,並歸類至現有或新的人物群組。", + "facial_recognition_job_description": "將偵測到的臉孔歸類為人物。此步驟會在臉孔偵測完成後執行。「重設」會重新對所有臉孔進行分群;「加入排程」則會將尚未指派人物的臉孔加入佇列。", "failed_job_command": "{job} 任務的 {command} 指令執行失敗", "force_delete_user_warning": "警告:這將立即刪除使用者及其所有項目。此動作無法復原,且無法找回已刪除的檔案。", "image_format": "格式", "image_format_description": "WebP 能產生相對於 JPEG 更小的檔案,但編碼速度較慢。", "image_fullsize_description": "移除中繼資料的大尺寸影像,在放大圖片時使用", "image_fullsize_enabled": "啟用大尺寸影像產生", - "image_fullsize_enabled_description": "為非網頁友善格式產生大尺寸相片。啟用「偏好內嵌預覽」時,系統將直接使用內嵌預覽而不進行轉碼,不影響 JPEG 等網頁友善格式。", + "image_fullsize_enabled_description": "為非網頁相容格式產生大尺寸相片。啟用「偏好內嵌預覽」時,系統將直接使用內嵌預覽而不進行轉碼,不影響 JPEG 等網頁相容格式。", "image_fullsize_quality_description": "大尺寸影像品質,範圍為 1 到 100。數值越高品質越好,但檔案也會越大。", "image_fullsize_title": "大尺寸影像設定", "image_prefer_embedded_preview": "偏好內嵌預覽", @@ -104,14 +104,14 @@ "image_preview_description": "中等尺寸影像(不含中繼資料),用於檢視單一項目與機器學習", "image_preview_quality_description": "預覽品質範圍為 1 到 100。數值越高品質越好,但檔案也會更大,並可能降低應用程式的回應速度。設定過低的數值可能會影響機器學習的品質。", "image_preview_title": "預覽設定", - "image_progressive": "逐步", - "image_progressive_description": "對 JPEG 影像進行漸進式編碼,以實現漸進式載入顯示。這不會影響 WebP 影像。", + "image_progressive": "漸進式", + "image_progressive_description": "對 JPEG 影像進行漸進式編碼,以達成漸進式載入顯示。這不會影響 WebP 影像。", "image_quality": "品質", "image_resolution": "解析度", "image_resolution_description": "較高的解析度能保留更多細節,但編碼時間會更長、檔案大小會更大,並可能降低應用程式的回應速度。", "image_settings": "圖片設定", "image_settings_description": "管理產生的影像品質與解析度", - "image_thumbnail_description": "移除中繼資料的小型縮圖,以用於檢視大量相片時使用,例如主時間軸", + "image_thumbnail_description": "已移除中繼資料的小型縮圖,用於檢視多張相片(如主時間軸)", "image_thumbnail_quality_description": "縮圖品質範圍為 1 到 100。數值越高品質越好,但檔案也會更大,並可能降低應用程式的回應速度。", "image_thumbnail_title": "縮圖設定", "import_config_from_json_description": "透過上傳 JSON 設定檔匯入系統設定", @@ -160,7 +160,7 @@ "machine_learning_facial_recognition": "人臉辨識", "machine_learning_facial_recognition_description": "偵測、辨識並對圖片中的臉孔分類", "machine_learning_facial_recognition_model": "人臉辨識模型", - "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。較大的模型速度較慢且佔用較多記憶體,但效果較佳。請注意,更換模型後必須對所有影像重新執行「臉孔偵測」任務。", + "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。較大的模型速度較慢且佔用較多記憶體,但結果較佳。請注意,更換模型後必須對所有影像重新執行「臉孔偵測」任務。", "machine_learning_facial_recognition_setting": "啟用人臉辨識", "machine_learning_facial_recognition_setting_description": "若停用,影像將不會進行人臉辨識編碼,且「探索」頁面的「人物」區塊將不會顯示任何內容。", "machine_learning_max_detection_distance": "偵測距離上限", @@ -173,7 +173,7 @@ "machine_learning_min_recognized_faces_description": "建立新人物所需的最低已辨識臉孔數量。提高此數值可讓臉孔辨識更精確,但同時會增加臉孔未被指派給任何人物的可能性。", "machine_learning_ocr": "文字辨識(OCR)", "machine_learning_ocr_description": "使用機器學習辨識影像中的文字", - "machine_learning_ocr_enabled": "啟用OCR", + "machine_learning_ocr_enabled": "啟用 OCR", "machine_learning_ocr_enabled_description": "若停用,影像將不會進行文字辨識。", "machine_learning_ocr_max_resolution": "最大解析度", "machine_learning_ocr_max_resolution_description": "解析度高於此值的預覽影像將在保持長寬比的情況下調整大小。數值越高越準確,但處理時間更長且會佔用更多記憶體。", @@ -181,7 +181,7 @@ "machine_learning_ocr_min_detection_score_description": "文字偵測的最低信心分數,範圍為 0 - 1。較低的數值會偵測到更多文字,但可能導致誤判。", "machine_learning_ocr_min_recognition_score": "最低辨識分數", "machine_learning_ocr_min_score_recognition_description": "已偵測文字的最低辨識信心分數,範圍為 0 - 1。較低的數值會辨識出更多文字,但可能導致誤判。", - "machine_learning_ocr_model": "OCR模型", + "machine_learning_ocr_model": "OCR 模型", "machine_learning_ocr_model_description": "伺服器模型比行動裝置模型更準確,但處理時間較長且會佔用更多記憶體。", "machine_learning_settings": "機器學習設定", "machine_learning_settings_description": "管理機器學習的功能和設定", @@ -257,7 +257,7 @@ "notification_email_password_description": "用於與電子郵件伺服器驗證的密碼", "notification_email_port_description": "電子郵件伺服器的連接埠(例如 25、465 或 587)", "notification_email_secure": "SMTPS", - "notification_email_secure_description": "使用SMTPS(基於TLS的SMTP)", + "notification_email_secure_description": "使用 SMTPS(基於 TLS 的 SMTP)", "notification_email_sent_test_email_button": "傳送測試電子郵件並儲存", "notification_email_setting_description": "寄送電子郵件通知的設定", "notification_email_test_email": "傳送測試電子郵件", @@ -281,11 +281,11 @@ "oauth_role_claim_description": "根據此宣告的存在,自動授予管理員權限。該宣告的值可以是 'user' 或 'admin'。", "oauth_settings": "OAuth", "oauth_settings_description": "管理 OAuth 登入設定", - "oauth_settings_more_details": "欲瞭解此功能,請參閱 說明書。", + "oauth_settings_more_details": "若要瞭解此功能的詳細資訊,請參閱 說明文件。", "oauth_storage_label_claim": "儲存標籤宣告", - "oauth_storage_label_claim_description": "自動將使用者的儲存標籤定為此宣告之值。", + "oauth_storage_label_claim_description": "自動將使用者的儲存標籤設定為此宣告之值。", "oauth_storage_quota_claim": "儲存配額宣告", - "oauth_storage_quota_claim_description": "自動將使用者的儲存配額定為此宣告之值。", + "oauth_storage_quota_claim_description": "自動將使用者的儲存配額設定為此宣告之值。", "oauth_storage_quota_default": "預設儲存配額(GiB)", "oauth_storage_quota_default_description": "未提供宣告時所使用的配額(GiB)。", "oauth_timeout": "請求逾時", @@ -297,8 +297,8 @@ "paths_validated_successfully": "所有路徑驗證成功", "person_cleanup_job": "清理人物", "queue_details": "佇列資訊", - "queues": "任務排程", - "queues_page_description": "序列排程管理界面", + "queues": "任務佇列", + "queues_page_description": "管理員任務佇列頁面", "quota_size_gib": "配額大小(GiB)", "refreshing_all_libraries": "正在重新整理所有媒體庫", "registration": "管理者註冊", @@ -320,8 +320,8 @@ "server_welcome_message": "歡迎訊息", "server_welcome_message_description": "在登入頁面顯示的訊息。", "settings_page_description": "管理設定頁面", - "sidecar_job": "側接檔案中繼資料", - "sidecar_job_description": "從檔案系統偵測或同步側接檔案中繼資料", + "sidecar_job": "附屬檔案中繼資料", + "sidecar_job_description": "從檔案系統偵測或同步附屬檔案中繼資料", "slideshow_duration_description": "每張圖片放映的秒數", "smart_search_job_description": "對項目執行機器學習以支援智慧搜尋", "storage_template_date_time_description": "檔案的建立時間戳會用於日期與時間資訊", @@ -428,8 +428,8 @@ "user_delete_delay": "{user} 的帳號和項目會在 {delay, plural, one {# 天} other {# 天}} 後永久刪除。", "user_delete_delay_settings": "延後刪除", "user_delete_delay_settings_description": "自移除後起算的天數,逾期後將永久刪除使用者帳號與項目。使用者刪除作業會在每日午夜執行,以檢查符合刪除條件的帳號。此設定的變更將在下一次執行時生效。", - "user_delete_immediately": "{user} 的帳號與項目將 立即 排入永久刪除序列。", - "user_delete_immediately_checkbox": "立即將使用者與項目排入永久刪除序列", + "user_delete_immediately": "{user} 的帳號與項目將 立即 排入永久刪除佇列。", + "user_delete_immediately_checkbox": "立即將使用者與項目排入永久刪除佇列", "user_details": "使用者詳細資訊", "user_management": "使用者管理", "user_password_has_been_reset": "使用者密碼已重設:", @@ -493,7 +493,7 @@ "album_selected": "已選取相簿", "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", "album_summary": "相簿摘要", - "album_updated": "更新相簿時", + "album_updated": "相簿已更新", "album_updated_setting_description": "當共享相簿有新項目時用電子郵件通知我", "album_upload_assets": "從您的電腦上傳檔案並加入相簿", "album_user_left": "離開 {album}", @@ -508,11 +508,11 @@ "album_viewer_page_share_add_users": "邀請其他人", "album_with_link_access": "任何擁有連結的人皆可檢視此相簿中的相片與人物。", "albums": "相簿", - "albums_count": "{count, plural, one {{count, number} 個相簿} other {{count, number} 個相簿}}", + "albums_count": "{count, plural, one {{count, number} 本相簿} other {{count, number} 本相簿}}", "albums_default_sort_order": "預設相簿排序", "albums_default_sort_order_description": "建立新相簿時要初始化項目排序方式。", "albums_feature_description": "可共享給其他使用者的項目集合。", - "albums_on_device_count": "此裝置有 ({count}) 個相簿", + "albums_on_device_count": "此裝置有 ({count}) 本相簿", "albums_selected": "{count, plural, one {已選取 # 本相簿} other {已選取 # 本相簿}}", "all": "全部", "all_albums": "所有相簿", @@ -591,7 +591,7 @@ "assets_added_to_album_count": "已將 {count, plural, one {# 個項目} other {# 個項目}}加入至相簿", "assets_added_to_albums_count": "已將 {assetTotal, plural, other {# 個項目}} 新增至 {albumTotal, plural, other {# 本相簿}}", "assets_cannot_be_added_to_album_count": "無法將 {count, plural, one {項目} other {項目}} 加入至相簿", - "assets_cannot_be_added_to_albums": "無法將 {count, plural, other {# 個項目}} 加入任何相簿", + "assets_cannot_be_added_to_albums": "無法將 {count, plural, one {項目} other {項目}} 加入任何相簿", "assets_count": "{count, plural, one {# 個項目} other {# 個項目}}", "assets_deleted_permanently": "已永久刪除 {count} 個項目", "assets_deleted_permanently_from_server": "已從 Immich 伺服器中永久移除 {count} 個項目", @@ -608,7 +608,7 @@ "assets_trashed_count": "已將 {count, plural, one {# 個項目} other {# 個項目}}移至垃圾桶", "assets_trashed_from_server": "已從 Immich 伺服器將 {count} 個項目移至垃圾桶", "assets_were_part_of_album_count": "{count, plural, one {該項目已} other {這些項目已}}在相簿中", - "assets_were_part_of_albums_count": "{count, plural, one {個} other {個}}項目已被儲存在相簿中", + "assets_were_part_of_albums_count": "{count, plural, one {該項目已} other {這些項目已}}存在於相簿中", "authorized_devices": "已授權裝置", "automatic_endpoint_switching_subtitle": "當可用時,透過指定的 Wi-Fi 在本機連線,其他情況則使用替代連線", "automatic_endpoint_switching_title": "自動 URL 切換", @@ -622,7 +622,7 @@ "backup": "備份", "backup_album_selection_page_albums_device": "裝置上的相簿({count})", "backup_album_selection_page_albums_tap": "點一下以選取,點兩下以排除", - "backup_album_selection_page_assets_scatter": "項目可以分散在多個相簿中,因此在備份過程中可以選擇納入或排除相簿。", + "backup_album_selection_page_assets_scatter": "項目可以分散在多本相簿中,因此在備份過程中可以選擇納入或排除相簿。", "backup_album_selection_page_select_albums": "選取相簿", "backup_album_selection_page_selection_info": "選取資訊", "backup_album_selection_page_total_assets": "總不重複項目數", @@ -761,11 +761,11 @@ "city": "城市", "cleanup_confirm_description": "Immich 發現有 {count} 個項目(建立於 {date} 之前)已安全備份至伺服器。是否要從此裝置中刪除本機副本?", "cleanup_confirm_prompt_title": "從此裝置刪除?", - "cleanup_deleted_assets": "已將{count}項目移到裝置的垃圾桶裡", + "cleanup_deleted_assets": "已將 {count} 個項目移到裝置的垃圾桶裡", "cleanup_deleting": "正在移動到垃圾桶...", - "cleanup_found_assets": "找到{count}件已上傳的項目", - "cleanup_found_assets_with_size": "找到{count}件,總共({size})已上傳的項目", - "cleanup_icloud_shared_albums_excluded": "iCloud共享相簿被排除於搜尋之外", + "cleanup_found_assets": "找到 {count} 件已上傳的項目", + "cleanup_found_assets_with_size": "找到 {count} 件,總共 ({size}) 已上傳的項目", + "cleanup_icloud_shared_albums_excluded": "iCloud 共享相簿被排除於搜尋之外", "cleanup_no_assets_found": "找不到符合上述條件的項目。釋放空間功能僅能移除已備份至伺服器的項目", "cleanup_preview_title": "{count} 項需要移除的項目", "cleanup_step3_description": "掃描符合日期與儲存設定的已備份項目。", @@ -782,8 +782,8 @@ "client_cert_import": "匯入", "client_cert_import_success_msg": "已匯入用戶端憑證", "client_cert_invalid_msg": "無效的憑證檔案或密碼錯誤", - "client_cert_password_message": "請輸入此證書的密碼", - "client_cert_password_title": "證書密碼", + "client_cert_password_message": "請輸入此憑證的密碼", + "client_cert_password_title": "憑證密碼", "client_cert_remove_msg": "用戶端憑證已移除", "client_cert_subtitle": "僅支援 PKCS12 (.p12, .pfx) 格式。憑證匯入與移除僅可在登入前進行", "client_cert_title": "SSL 用戶端憑證 [實驗性]", @@ -794,7 +794,7 @@ "color": "顏色", "color_theme": "色彩主題", "command": "命令", - "command_palette_prompt": "快速尋找頁面,動作或者指令", + "command_palette_prompt": "快速搜尋頁面、動作或指令", "command_palette_to_close": "關閉", "command_palette_to_navigate": "輸入", "command_palette_to_select": "選擇", @@ -837,7 +837,7 @@ "copy_password": "複製密碼", "copy_to_clipboard": "複製到剪貼簿", "country": "國家", - "cover": "封面", + "cover": "填滿", "covers": "封面", "create": "建立", "create_album": "建立相簿", @@ -849,9 +849,12 @@ "create_link_to_share": "建立分享連結", "create_link_to_share_description": "持有連結的人皆可檢視所選項目", "create_new": "新增", + "create_new_face": "建立新臉孔", "create_new_person": "建立新人物", "create_new_person_hint": "將選取的項目指派給新的人物", "create_new_user": "建立新使用者", + "create_person": "建立人物", + "create_person_subtitle": "為所選臉孔新增名字以建立和標記新人物", "create_shared_album_page_share_add_assets": "新增項目", "create_shared_album_page_share_select_photos": "選取相片", "create_shared_link": "建立分享連結", @@ -892,7 +895,7 @@ "day": "日", "days": "日", "deduplicate_all": "刪除所有重複項目", - "default_locale": "默認語言", + "default_locale": "預設語言", "default_locale_description": "使用你的瀏覽器區域以格式日期和數字", "delete": "刪除", "delete_action_confirmation_message": "您確定要刪除此項目嗎?此動作會將該項目移至伺服器的垃圾桶,並詢問您是否要在本機同步刪除", @@ -965,7 +968,7 @@ "download_waiting_to_retry": "等待重試", "downloading": "下載中", "downloading_asset_filename": "正在下載項目 {filename}", - "downloading_from_icloud": "正從iCloud下載", + "downloading_from_icloud": "正從 iCloud 下載", "downloading_media": "正在下載媒體", "drop_files_to_upload": "將檔案拖放到任何位置以上傳", "duplicates": "重複項目", @@ -1010,8 +1013,8 @@ "editor_handle_edge": "{edge, select, top {頂部} bottom {底部} left {左側} right {右側} other {某個}} 邊緣的控制手柄", "editor_orientation": "方向", "editor_reset_all_changes": "重設變更", - "editor_rotate_left": "逆時針旋轉90度", - "editor_rotate_right": "順時針旋轉90度", + "editor_rotate_left": "逆時針旋轉 90 度", + "editor_rotate_right": "順時針旋轉 90 度", "email": "電子郵件", "email_notifications": "電子郵件通知", "empty_folder": "這個資料夾是空的", @@ -1022,7 +1025,7 @@ "enable_biometric_auth_description": "輸入您的 PIN 碼以啟用生物辨識驗證", "enabled": "已啟用", "end_date": "結束日期", - "enqueued": "已排入序列", + "enqueued": "已排入佇列", "enter_wifi_name": "輸入 Wi-Fi 名稱", "enter_your_pin_code": "輸入您的 PIN 碼", "enter_your_pin_code_subtitle": "輸入您的 PIN 碼以存取「已鎖定」資料夾", @@ -1344,11 +1347,11 @@ "ios_debug_info_processing_ran_at": "於 {dateTime} 執行處理", "items_count": "{count, plural, one {# 個項目} other {# 個項目}}", "jobs": "任務", - "json_editor": "JSON編輯器", - "json_error": "JSON錯誤", + "json_editor": "JSON 編輯器", + "json_error": "JSON 錯誤", "keep": "保留", "keep_albums": "保留相簿", - "keep_albums_count": "保留{count} {count, plural, one {個相簿} other {個相簿}}", + "keep_albums_count": "保留{count} {count, plural, one {本相簿} other {本相簿}}", "keep_all": "全部保留", "keep_description": "選擇執行釋放空間時要保留在裝置上的項目。", "keep_favorites": "保留最愛的相片", @@ -1356,7 +1359,7 @@ "keep_on_device_hint": "選擇保留在裝置上的相片", "keep_this_delete_others": "保留這個,刪除其他", "keeping": "保留:{items}", - "kept_this_deleted_others": "保留這個項目並刪除{count, plural, one {# asset} other {# assets}}", + "kept_this_deleted_others": "保留這個項目並刪除{count, plural, one {# 個項目} other {# 個項目}}", "keyboard_shortcuts": "鍵盤快捷鍵", "language": "語言", "language_no_results_subtitle": "試著調整您的搜尋詞彙", @@ -1390,7 +1393,7 @@ "like": "喜歡", "like_deleted": "已取消喜歡", "link_motion_video": "連結動態影片", - "link_to_docs": "請參閱 文案 以獲取更多信息。", + "link_to_docs": "請參閱 說明文件 以獲取更多資訊。", "link_to_oauth": "連結 OAuth", "linked_oauth_account": "已連結 OAuth 帳號", "list": "清單", @@ -1399,7 +1402,7 @@ "local": "本機", "local_asset_cast_failed": "無法投放未上傳至伺服器的項目", "local_assets": "本機項目", - "local_id": "本地ID", + "local_id": "本地 ID", "local_media_summary": "本機媒體摘要", "local_network": "本機網路", "local_network_sheet_info": "當使用指定的 Wi-Fi 網路時,應用程式將透過此網址連線至伺服器", @@ -1488,14 +1491,14 @@ "manage_your_devices": "管理已登入的裝置", "manage_your_oauth_connection": "管理您的 OAuth 連結", "map": "地圖", - "map_assets_in_bounds": "{count, plural, one {# 張相片} other {# 張相片}}", + "map_assets_in_bounds": "{count, plural, =0 {此區域沒有相片} one {# 張相片} other {# 張相片}}", "map_cannot_get_user_location": "無法取得使用者位置", "map_location_dialog_yes": "確定", "map_location_picker_page_use_location": "使用此位置", "map_location_service_disabled_content": "需要啟用定位服務才能顯示您目前位置相關的項目。要現在啟用嗎?", "map_location_service_disabled_title": "定位服務已停用", - "map_marker_for_images": "在 {city}、{country} 拍攝影像的地圖示記", - "map_marker_with_image": "帶有影像的地圖示記", + "map_marker_for_images": "在 {city}、{country} 拍攝影像的地圖標記", + "map_marker_with_image": "帶有影像的地圖標記", "map_no_location_permission_content": "需要位置權限才能顯示與您目前位置相關的項目。要現在就授予位置權限嗎?", "map_no_location_permission_title": "沒有位置權限", "map_settings": "地圖設定", @@ -1553,7 +1556,7 @@ "move_to_locked_folder_confirmation": "這些相片與影片將從所有相簿中移除,且僅能從「已鎖定」資料夾中檢視", "move_up": "向上移動", "moved_to_archive": "已封存 {count, plural, one {# 個項目} other {# 個項目}}", - "moved_to_library": "已移動 {count, plural, one {# 個項目} other {# 個項目}} 至相簿", + "moved_to_library": "已移動 {count, plural, one {# 個項目} other {# 個項目}} 至媒體庫", "moved_to_trash": "已丟進垃圾桶", "multiselect_grid_edit_date_time_err_read_only": "唯讀項目的日期無法編輯,已略過", "multiselect_grid_edit_gps_err_read_only": "唯讀項目的位置資訊無法編輯,已略過", @@ -1562,12 +1565,12 @@ "name": "名稱", "name_or_nickname": "名稱或暱稱", "name_required": "名稱是必填項", - "navigate": "導航", + "navigate": "導覽", "navigate_to_time": "跳轉至指定時間", "network_requirement_photos_upload": "使用行動網路流量備份相片", "network_requirement_videos_upload": "使用行動網路流量備份影片", "network_requirements": "網路要求", - "network_requirements_updated": "網路需求已變更,正在重設備份序列", + "network_requirements_updated": "網路需求已變更,正在重設備份佇列", "networking_settings": "網路", "networking_subtitle": "管理伺服器端點設定", "never": "永不失效", @@ -1591,7 +1594,7 @@ "no_albums_message": "建立相簿來整理相片和影片", "no_albums_with_name_yet": "看來還沒有這個名字的相簿。", "no_albums_yet": "看來您還沒有任何相簿。", - "no_archived_assets_message": "將相片與影片封存後,就不會顯示在「相片」視圖中", + "no_archived_assets_message": "將相片與影片封存後,就不會顯示在「相片」頁面中", "no_assets_message": "按這裡上傳您的第一張相片", "no_assets_to_show": "無項目展示", "no_cast_devices_found": "找不到 Google Cast 裝置", @@ -1707,7 +1710,7 @@ "permanent_deletion_warning_setting_description": "在永久刪除檔案時顯示警告", "permanently_delete": "永久刪除", "permanently_delete_assets_count": "永久刪除 {count, plural, one {檔案} other {檔案}}", - "permanently_delete_assets_prompt": "確定要永久刪除 {count, plural, other {這 # 個檔案?}}這樣{count, plural, one {它} other {它們}}也會從自己所在的相簿中消失。", + "permanently_delete_assets_prompt": "確定要永久刪除 {count, plural, one {這個檔案?} other {這 # 個檔案?}}這樣{count, plural, one {它} other {它們}}也會從自己所在的相簿中消失。", "permanently_deleted_asset": "永久刪除的檔案", "permanently_deleted_assets_count": "永久刪除的 {count, plural, one {# 個檔案} other {# 個檔案}}", "permission": "權限", @@ -1770,7 +1773,7 @@ "profile_drawer_app_logs": "紀錄", "profile_drawer_client_server_up_to_date": "用戶端與伺服器版本皆為最新", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "唯讀模式已啟用。長按使用者個人圖示即可退出。", + "profile_drawer_readonly_mode": "唯讀模式已啟用。長按使用者個人圖示即可關閉。", "profile_image_of_user": "{user} 的個人資料圖片", "profile_picture_set": "已設定個人資料圖片。", "public_album": "公開相簿", @@ -1812,7 +1815,7 @@ "rate_asset": "項目評分", "rating": "評星", "rating_clear": "清除評等", - "rating_count": "{count, plural, =0 {Unrated} other {# 星}}", + "rating_count": "{count, plural, =0 {未評分} one {# 星} other {# 星}}", "rating_description": "在資訊面板中顯示 EXIF 評等", "reaction_options": "反應選項", "read_changelog": "閱覽更新紀錄", @@ -1832,15 +1835,15 @@ "recently_taken_page_title": "最近拍攝", "refresh": "重新整理", "refresh_encoded_videos": "重新整理已編碼的影片", - "refresh_faces": "重整面部資料", + "refresh_faces": "重新整理臉孔資料", "refresh_metadata": "重新整理中繼資料", "refresh_thumbnails": "重新整理縮圖", "refreshed": "重新整理完畢", "refreshes_every_file": "重新讀取所有現有與新增檔案", "refreshing_encoded_video": "正在重新整理已編碼的影片", - "refreshing_faces": "重整面部資料中", + "refreshing_faces": "正在重新整理臉孔資料", "refreshing_metadata": "正在重新整理中繼資料", - "regenerating_thumbnails": "重新產生縮圖中", + "regenerating_thumbnails": "正在重新產生縮圖", "remote": "遠端", "remote_assets": "遠端項目", "remote_media_summary": "遠端媒體摘要", @@ -1869,8 +1872,8 @@ "removed_memory": "已移除記憶", "removed_photo_from_memory": "已從記憶中移除相片", "removed_tagged_assets": "已移除 {count, plural, one {# 個檔案} other {# 個檔案}}的標籤", - "rename": "改名", - "repair": "糾正", + "rename": "重新命名", + "repair": "修復", "repair_no_results_message": "未被追蹤及遺失的檔案會顯示在這裡", "replace_with_upload": "用上傳的檔案取代", "repository": "儲存庫", @@ -1886,9 +1889,9 @@ "reset_pin_code_with_password": "您可隨時使用您的密碼來重設 PIN 碼", "reset_sqlite": "重設 SQLite 資料庫", "reset_sqlite_clear_app_data": "清除資料", - "reset_sqlite_confirmation": "確定要重設所有資料嗎?你的所有設置將被重設,且你會被登出。", - "reset_sqlite_confirmation_note": "注意:你需要在清除資料後重新開啟應用。", - "reset_sqlite_done": "資料已清除。請重啟Immich及重新登入。", + "reset_sqlite_confirmation": "確定要重設所有資料嗎?你的所有設定將被重設,且你會被登出。", + "reset_sqlite_confirmation_note": "注意:你需要在清除資料後重新開啟 App。", + "reset_sqlite_done": "資料已清除。請重啟 Immich 及重新登入。", "reset_sqlite_success": "已成功重設 SQLite 資料庫", "reset_to_default": "重設為預設值", "resolution": "解析度", @@ -1910,7 +1913,7 @@ "running": "執行中", "save": "儲存", "save_to_gallery": "儲存到相簿", - "saved": "已保存", + "saved": "已儲存", "saved_api_key": "已儲存 API 金鑰", "saved_profile": "已儲存個人資料", "saved_settings": "已儲存設定", @@ -1930,9 +1933,9 @@ "search_by_description_example": "在沙壩的健行之日", "search_by_filename": "依檔名或副檔名搜尋", "search_by_filename_example": "如 IMG_1234.JPG 或 PNG", - "search_by_ocr": "透過OCR搜尋", + "search_by_ocr": "透過 OCR 搜尋", "search_by_ocr_example": "拿鐵", - "search_camera_lens_model": "蒐索鏡頭型號...", + "search_camera_lens_model": "搜尋鏡頭型號...", "search_camera_make": "搜尋相機製造商…", "search_camera_model": "搜尋相機型號…", "search_city": "搜尋城市…", @@ -1949,7 +1952,7 @@ "search_filter_location_title": "選擇位置", "search_filter_media_type": "媒體類型", "search_filter_media_type_title": "選擇媒體類型", - "search_filter_ocr": "透過OCR搜尋", + "search_filter_ocr": "透過 OCR 搜尋", "search_filter_people_title": "選擇人物", "search_filter_star_rating": "評分", "search_filter_tags_title": "選擇標籤", @@ -1978,7 +1981,7 @@ "search_settings": "搜尋設定", "search_state": "搜尋地區…", "search_suggestion_list_smart_search_hint_1": "智慧搜尋功能預設已啟用,如要搜尋中繼資料,請使用語法 ", - "search_suggestion_list_smart_search_hint_2": "m:您的搜尋關鍵詞", + "search_suggestion_list_smart_search_hint_2": "m:您的搜尋關鍵字", "search_tags": "搜尋標籤...", "search_timezone": "搜尋時區…", "search_type": "搜尋類型", @@ -2032,7 +2035,7 @@ "set_profile_picture": "設定個人資料圖片", "set_slideshow_to_fullscreen": "以全螢幕放映幻燈片", "set_stack_primary_asset": "設定堆疊的首要項目", - "setting_image_navigation_enable_subtitle": "開啟後以觸碰屏幕左/右邊緣區域的方式切換上/下圖片。", + "setting_image_navigation_enable_subtitle": "開啟後以觸碰螢幕左/右邊緣區域的方式切換上/下圖片。", "setting_image_navigation_enable_title": "點擊切換", "setting_image_navigation_title": "圖片導引", "setting_image_viewer_help": "詳細資訊檢視器會依序載入小型縮圖、中等尺寸預覽圖(若啟用),最後載入原始相片。", @@ -2132,7 +2135,7 @@ "show_all_people": "顯示所有人物", "show_and_hide_people": "顯示與隱藏人物", "show_file_location": "顯示檔案位置", - "show_gallery": "顯示畫廊", + "show_gallery": "顯示媒體庫", "show_hidden_people": "顯示隱藏的人物", "show_in_timeline": "在時間軸中顯示", "show_in_timeline_setting_description": "在您的時間軸中顯示這位使用者的相片和影片", @@ -2149,7 +2152,7 @@ "show_supporter_badge": "支持者徽章", "show_supporter_badge_description": "顯示支持者徽章", "show_text_recognition": "顯示文字辨識", - "show_text_search_menu": "顯示文字蒐索選單", + "show_text_search_menu": "顯示文字搜尋選單", "shuffle": "隨機排序", "sidebar": "側邊欄", "sidebar_display_description": "在側邊欄中顯示連結", @@ -2214,6 +2217,7 @@ "tag": "標籤", "tag_assets": "標記檔案", "tag_created": "已建立標籤:{tag}", + "tag_face": "標記臉孔", "tag_feature_description": "以邏輯標記要旨分類瀏覽相片和影片", "tag_not_found_question": "找不到標籤?建立新標籤。", "tag_people": "標籤人物", @@ -2264,11 +2268,11 @@ "trash_all": "全部丟掉", "trash_count": "丟掉 {count, number} 個檔案", "trash_delete_asset": "將檔案丟進垃圾桶 / 刪除", - "trash_emptied": "已清空回收桶", + "trash_emptied": "已清空垃圾桶", "trash_no_results_message": "垃圾桶中的相片和影片將顯示在這裡。", "trash_page_delete_all": "刪除全部", - "trash_page_empty_trash_dialog_content": "是否清空回收桶?這些項目將被從 Immich 中永久刪除", - "trash_page_info": "回收桶中項目將在 {days} 天後永久刪除", + "trash_page_empty_trash_dialog_content": "是否清空垃圾桶?這些項目將被從 Immich 中永久刪除", + "trash_page_info": "垃圾桶中項目將在 {days} 天後永久刪除", "trash_page_no_assets": "暫無已刪除項目", "trash_page_restore_all": "全部還原", "trash_page_select_assets_btn": "選擇項目", @@ -2281,7 +2285,7 @@ "trigger_person_recognized": "已辨識人物", "trigger_person_recognized_description": "偵測到人物時觸發", "trigger_type": "觸發類型", - "troubleshoot": "疑難解答", + "troubleshoot": "疑難排解", "type": "類型", "unable_to_change_pin_code": "無法變更 PIN 碼", "unable_to_check_version": "無法檢查應用程式或伺服器版本", @@ -2313,7 +2317,7 @@ "unstack_action_prompt": "{count} 個取消堆疊", "unstacked_assets_count": "已解除堆疊 {count, plural, other {# 個檔案}}", "unsupported_field_type": "不支援的欄位類型", - "unsupported_file_type": "不支持 {type} 類型的檔案,無法上傳 {file} 文件。", + "unsupported_file_type": "不支援 {type} 類型的檔案,無法上傳 {file} 檔案。", "untagged": "無標籤", "untitled_workflow": "未命名工作流程", "up_next": "下一個", @@ -2371,7 +2375,7 @@ "version_history": "版本紀錄", "version_history_item": "{date} 安裝了 {version}", "video": "影片", - "video_hover_setting": "遊標停留時播放影片縮圖", + "video_hover_setting": "游標停留時播放影片縮圖", "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用此功能,仍可透過將滑鼠停在播放圖示上來開始播放。", "videos": "影片", "videos_count": "{count, plural, other {# 部影片}}", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 89480a8cb8..8126ff0859 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,8 +1,8 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:aa23850b91cb4c7faedac8ca9aa74ddc6eb03529a519145a589a7f35df4c5927 AS builder-cpu +FROM python:3.11-bookworm@sha256:970c99f886b839fc8829289040c1845dadaf2cae46b37acc7710333158ec29b4 AS builder-cpu -FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS builder-openvino +FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS builder-openvino FROM builder-cpu AS builder-cuda @@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy -FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:9c6f90801e6b68e772b7c0ca74260cbf7af9f320acec894e26fccdaccfbe3b47 AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \ MACHINE_LEARNING_MODEL_ARENA=false -FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS prod-openvino +FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 8f559589a9..640996f54a 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "immich-ml" -version = "2.7.4" +version = "2.7.5" description = "" authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }] requires-python = ">=3.11,<4.0" @@ -14,7 +14,7 @@ dependencies = [ "numpy<2.4.0", "opencv-python-headless>=4.7.0.72,<5.0", "orjson>=3.9.5", - "pillow>=12.1.1,<12.2", + "pillow>=12.2,<12.3", "pydantic>=2.0.0,<3", "pydantic-settings>=2.5.2,<3", "python-multipart>=0.0.6,<1.0", diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 77ffd247a6..894acf77f5 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -511,7 +511,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd [[package]] name = "fastapi" -version = "0.128.8" +version = "0.136.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -520,9 +520,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, + { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, ] [[package]] @@ -764,14 +764,14 @@ wheels = [ [[package]] name = "gunicorn" -version = "25.1.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, + { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, ] [[package]] @@ -898,7 +898,7 @@ wheels = [ [[package]] name = "immich-ml" -version = "2.7.4" +version = "2.7.5" source = { editable = "." } dependencies = [ { name = "aiocache" }, @@ -996,7 +996,7 @@ requires-dist = [ { name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "orjson", specifier = ">=3.9.5" }, - { name = "pillow", specifier = ">=12.1.1,<12.2" }, + { name = "pillow", specifier = ">=12.2,<12.3" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "pydantic-settings", specifier = ">=2.5.2,<3" }, { name = "python-multipart", specifier = ">=0.0.6,<1.0" }, @@ -1160,70 +1160,80 @@ wheels = [ [[package]] name = "librt" -version = "0.7.4" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, - { url = "https://files.pythonhosted.org/packages/d0/32/0b2296f9cc7e693ab0d0835e355863512e5eac90450c412777bd699c76ae/librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6", size = 171027, upload-time = "2025-12-15T16:51:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/d8/33/c70b6d40f7342716e5f1353c8da92d9e32708a18cbfa44897a93ec2bf879/librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82", size = 184700, upload-time = "2025-12-15T16:51:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" }, - { url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" }, - { url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" }, - { url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" }, - { url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" }, - { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" }, - { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, - { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, - { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, - { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, - { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, - { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, - { url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, - { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, - { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, - { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, - { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, - { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, - { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" }, - { url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" }, - { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, - { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, - { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, - { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, - { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, + { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] [[package]] name = "locust" -version = "2.43.3" +version = "2.43.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1243,9 +1253,9 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/c5/7d7bd50ac744bc209a4bcbeb74660d7ae450a44441737efe92ee9d8ea6a7/locust-2.43.3.tar.gz", hash = "sha256:b5d2c48f8f7d443e3abdfdd6ec2f7aebff5cd74fab986bcf1e95b375b5c5a54b", size = 1445349, upload-time = "2026-02-12T09:55:34.591Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/be/6df1c778f673e1e2d785f262d20a4e130fdb8e51242466d7ae434b66a587/locust-2.43.4.tar.gz", hash = "sha256:4ace60f07f5fa9bf08d1b64da25915707befca19a790897eed6372656824deee", size = 1434321, upload-time = "2026-04-01T20:43:04.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d2/dc5379876d3a481720803653ea4d219f0c26f2d2b37c9243baaa16d0bc79/locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553", size = 1463473, upload-time = "2026-02-12T09:55:31.727Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/a90d0b6fc476eb0f8e5a705f49e410563450b9b087688cc93a50eab54d63/locust-2.43.4-py3-none-any.whl", hash = "sha256:a4f40403e9f665e0dcb94991d9a8f19317d0d36afe88400833c5fab99ba942ed", size = 1454332, upload-time = "2026-04-01T20:43:02.767Z" }, ] [[package]] @@ -1462,7 +1472,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -1470,33 +1480,44 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, + { url = "https://files.pythonhosted.org/packages/82/0d/555ab7453cc4a4a8643b7f21c842b1a84c36b15392061ae7b052ee119320/mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e", size = 14336012, upload-time = "2026-04-13T02:45:39.935Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/85a28893f7db8a16ebb41d1e9dfcb4475844d06a88480b6639e32a74d6ef/mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca", size = 13224636, upload-time = "2026-04-13T02:45:49.659Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/bd4cd3c2caeb6c448b669222b8cfcbdee4a03b89431527b56fca9e56b6f3/mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955", size = 13663471, upload-time = "2026-04-13T02:46:20.276Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/7ee8c471e10402d64b6517ae10434541baca053cffd81090e4097d5609d4/mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8", size = 14532344, upload-time = "2026-04-13T02:46:44.205Z" }, + { url = "https://files.pythonhosted.org/packages/b5/95/b37d1fa859a433f6156742e12f62b0bb75af658544fb6dada9363918743a/mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65", size = 14776670, upload-time = "2026-04-13T02:45:52.481Z" }, + { url = "https://files.pythonhosted.org/packages/03/77/b302e4cb0b80d2bdf6bf4fce5864bb4cbfa461f7099cea544eaf2457df78/mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2", size = 10816524, upload-time = "2026-04-13T02:45:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/d969d7a68eb964993ebcc6170d5ecaf0cf65830c58ac3344562e16dc42a9/mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10", size = 9750419, upload-time = "2026-04-13T02:45:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, + { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" }, + { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" }, + { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" }, + { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, ] [[package]] @@ -1650,7 +1671,7 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.24.1" +version = "1.24.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, @@ -1660,31 +1681,35 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/88/d9757c62a0f96b5193f8d447a141eefd14498c404cc5caf1a6f3233cf102/onnxruntime-1.24.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:79b3119ab9f4f3817062e6dbe7f4a44937de93905e3a31ba34313d18cb49e7be", size = 17212018, upload-time = "2026-02-05T17:32:13.986Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/b3305c39144e19dbe8791802076b29b4b592b09de03d0e340c1314bfd408/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86bc43e922b1f581b3de26a3dc402149c70e5542fceb5bec6b3a85542dbeb164", size = 15018703, upload-time = "2026-02-05T17:30:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/94/d6/d273b75fe7825ea3feed321dd540aef33d8a1380ddd8ac3bb70a8ed000fe/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cabe71ca14dcfbf812d312aab0a704507ac909c137ee6e89e4908755d0fc60e", size = 17096352, upload-time = "2026-02-05T17:31:29.057Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/0616101a3938bfe2918ea60b581a9bbba61ffc255c63388abb0885f7ce18/onnxruntime-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:3273c330f5802b64b4103e87b5bbc334c0355fff1b8935d8910b0004ce2f20c8", size = 12493235, upload-time = "2026-02-05T17:32:04.451Z" }, - { url = "https://files.pythonhosted.org/packages/c8/30/437de870e4e1c6d237a2ca5e11f54153531270cb5c745c475d6e3d5c5dcf/onnxruntime-1.24.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7307aab9e2e879c0171f37e0eb2808a5b4aec7ba899bb17c5f0cedfc301a8ac2", size = 17211043, upload-time = "2026-02-05T17:32:16.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/60/004401cd86525101ad8aa9eec301327426555d7a77fac89fd991c3c7aae6/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780add442ce2d4175fafb6f3102cdc94243acffa3ab16eacc03dd627cc7b1b54", size = 15016224, upload-time = "2026-02-05T17:30:56.791Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a1/43ad01b806a1821d1d6f98725edffcdbad54856775643718e9124a09bfbe/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6119526eda12613f0d0498e2ae59563c247c370c9cef74c2fc93133dde157", size = 17098191, upload-time = "2026-02-05T17:31:31.87Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/5beb65270864037d5c8fb25cfe6b23c48b618d1f4d06022d425cbf29bd9c/onnxruntime-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0af2f1cfcfff9094971c7eb1d1dfae7ccf81af197493c4dc4643e4342c0946", size = 12493108, upload-time = "2026-02-05T17:32:07.076Z" }, - { url = "https://files.pythonhosted.org/packages/95/77/7172ecfcbdabd92f338e694f38c325f6fab29a38fa0a8c3d1c85b9f4617c/onnxruntime-1.24.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:82e367770e8fba8a87ba9f4c04bb527e6d4d7204540f1390f202c27a3b759fb4", size = 17211381, upload-time = "2026-02-05T17:31:09.601Z" }, - { url = "https://files.pythonhosted.org/packages/79/5b/532a0d75b93bbd0da0e108b986097ebe164b84fbecfdf2ddbf7c8a3a2e83/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1099f3629832580fedf415cfce2462a56cc9ca2b560d6300c24558e2ac049134", size = 15016000, upload-time = "2026-02-05T17:31:00.116Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b5/40606c7bce0702975a077bc6668cd072cd77695fc5c0b3fcf59bdb1fe65e/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6361dda4270f3939a625670bd67ae0982a49b7f923207450e28433abc9c3a83b", size = 17097637, upload-time = "2026-02-05T17:31:34.787Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/9e8f7933796b466241b934585723c700d8fb6bde2de856e65335193d7c93/onnxruntime-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:bd1e4aefe73b6b99aa303cd72562ab6de3cccb09088100f8ad1c974be13079c7", size = 12492467, upload-time = "2026-02-05T17:32:09.834Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8a/ee07d86e35035f9fed42497af76435f5a613d4e8b6c537ea0f8ef9fa85da/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88a2b54dca00c90fca6303eedf13d49b5b4191d031372c2e85f5cffe4d86b79e", size = 15025407, upload-time = "2026-02-05T17:31:02.251Z" }, - { url = "https://files.pythonhosted.org/packages/fd/9e/ab3e1dda4b126313d240e1aaa87792ddb1f5ba6d03ca2f093a7c4af8c323/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dfbba602da840615ed5b431facda4b3a43b5d8276cf9e0dbf13d842df105838", size = 17099810, upload-time = "2026-02-05T17:31:37.537Z" }, - { url = "https://files.pythonhosted.org/packages/87/23/167d964414cee2af9c72af323b28d2c4cb35beed855c830a23f198265c79/onnxruntime-1.24.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:890c503ca187bc883c3aa72c53f2a604ec8e8444bdd1bf6ac243ec6d5e085202", size = 17214004, upload-time = "2026-02-05T17:31:11.917Z" }, - { url = "https://files.pythonhosted.org/packages/b4/24/6e5558fdd51027d6830cf411bc003ae12c64054826382e2fab89e99486a0/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da1b84b3bdeec543120df169e5e62a1445bf732fc2c7fb036c2f8a4090455e8", size = 15017034, upload-time = "2026-02-05T17:31:04.331Z" }, - { url = "https://files.pythonhosted.org/packages/91/d4/3cb1c9eaae1103265ed7eb00a3eaeb0d9ba51dc88edc398b7071c9553bed/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:557753ec345efa227c6a65139f3d29c76330fcbd54cc10dd1b64232ebb939c13", size = 17097531, upload-time = "2026-02-05T17:31:40.303Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/4522b199c12db7c5b46aaf265ee0d741abe65ea912f6c0aaa2cc18a4654d/onnxruntime-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:ea4942104805e868f3ddddfa1fbb58b04503a534d489ab2d1452bbfa345c78c2", size = 12795556, upload-time = "2026-02-05T17:32:11.886Z" }, - { url = "https://files.pythonhosted.org/packages/a1/53/3b8969417276b061ff04502ccdca9db4652d397abbeb06c9f6ae05cec9ca/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea8963a99e0f10489acdf00ef3383c3232b7e44aa497b063c63be140530d9f85", size = 15025434, upload-time = "2026-02-05T17:31:06.942Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/cfcf009eb38d90cc628c087b6506b3dfe1263387f3cbbf8d272af4fef957/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34488aa760fb5c2e6d06a7ca9241124eb914a6a06f70936a14c669d1b3df9598", size = 17099815, upload-time = "2026-02-05T17:31:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" }, + { url = "https://files.pythonhosted.org/packages/38/e9/8c901c150ce0c368da38638f44152fb411059c0c7364b497c9e5c957321a/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7", size = 15152472, upload-time = "2026-03-17T22:03:26.176Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b6/7a4df417cdd01e8f067a509e123ac8b31af450a719fa7ed81787dd6057ec/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330", size = 17222993, upload-time = "2026-03-17T22:04:34.485Z" }, + { url = "https://files.pythonhosted.org/packages/dd/59/8febe015f391aa1757fa5ba82c759ea4b6c14ef970132efb5e316665ba61/onnxruntime-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153", size = 12594863, upload-time = "2026-03-17T22:05:38.749Z" }, + { url = "https://files.pythonhosted.org/packages/32/84/4155fcd362e8873eb6ce305acfeeadacd9e0e59415adac474bea3d9281bb/onnxruntime-1.24.4-cp311-cp311-win_arm64.whl", hash = "sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b", size = 12259895, upload-time = "2026-03-17T22:05:28.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/6b89e297b93704345f0f3f8c62229bee323ef25682a3f9b4f89a39324950/onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb", size = 12596856, upload-time = "2026-03-17T22:05:41.224Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/8b8ec6e9e6a474fcd5d772453f627ad4549dfe3ab8c0bf70af5afcde551b/onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90", size = 12270275, upload-time = "2026-03-17T22:05:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, + { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300, upload-time = "2026-03-17T22:03:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936, upload-time = "2026-03-17T22:03:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432, upload-time = "2026-03-17T22:04:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276, upload-time = "2026-03-17T22:05:46.349Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365, upload-time = "2026-03-17T22:05:35.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889, upload-time = "2026-03-17T22:03:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, ] [[package]] name = "onnxruntime-gpu" -version = "1.24.1" +version = "1.24.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, @@ -1694,16 +1719,16 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/c7/07d06175f1124fc89e8b7da30d70eb8e0e1400d90961ae1cbea9da69e69b/onnxruntime_gpu-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac4bfc90c376516b13d709764ab257e4e3d78639bf6a2ccfc826e9db4a5c7ddf", size = 252616647, upload-time = "2026-02-05T17:24:02.993Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/47c2a873bf5fc307cda696e8a8cb54b7c709f5a4b3f9e2b4a636066a63c2/onnxruntime_gpu-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:ccd800875cb6c04ce623154c7fa312da21631ef89a9543c9a21593817cfa3473", size = 207089749, upload-time = "2026-02-05T17:23:59.5Z" }, - { url = "https://files.pythonhosted.org/packages/db/a8/fb1a36a052321a839cc9973f6cfd630709412a24afff2d7315feb3efc4b8/onnxruntime_gpu-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:710bf83751e6761584ad071102af3cbffd4b42bb77b2e3caacfb54ffbaa0666b", size = 252628733, upload-time = "2026-02-05T17:24:12.926Z" }, - { url = "https://files.pythonhosted.org/packages/52/65/48f694b81a963f3ee575041d5f2879b15268f5e7e14d90c3e671836c9646/onnxruntime_gpu-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:b128a42b3fa098647765ba60c2af9d4bf839181307cfac27da649364feb37f7b", size = 207089008, upload-time = "2026-02-05T17:24:07.126Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e7/4e19062e95d3701c0d32c228aa848ba4a1cc97651e53628d978dba8e1267/onnxruntime_gpu-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db9acb0d0e59d93b4fa6b7fd44284ece4408d0acee73235d43ed343f8cee7ee5", size = 252629216, upload-time = "2026-02-05T17:24:24.604Z" }, - { url = "https://files.pythonhosted.org/packages/c4/82/223d7120d8a98b07c104ddecfb0cc2536188e566a4e9c2dee7572453f89c/onnxruntime_gpu-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:59fdb40743f0722f3b859209f649ea160ca6bb42799e43f49b70a3ec5fc8c4ad", size = 207089285, upload-time = "2026-02-05T17:24:18.497Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/3159e57f09d7e6c8ad47d8ba8d5bd7494f383bc1071481cf38c9c8142bf9/onnxruntime_gpu-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88ca04e1dffea2d4c3c79cf4de7f429e99059d085f21b3e775a8d36380cd5186", size = 252633977, upload-time = "2026-02-05T17:24:33.568Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b4/51ad0ab878ff1456a831a0566b4db982a904e22f138e4b2c5f021bac517f/onnxruntime_gpu-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ced66900b1f48bddb62b5233925c3b56f8e008e2c34ebf8c060b20cae5842bcf", size = 252629039, upload-time = "2026-02-05T17:24:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/9c/46/336d4e09a6af66532eedde5c8f03a73eaa91a046b408522259ab6a604363/onnxruntime_gpu-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:129f6ae8b331a6507759597cd317b23e94aed6ead1da951f803c3328f2990b0c", size = 209487551, upload-time = "2026-02-05T17:24:26.373Z" }, - { url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/e080d758f2b60f71abe518c707135fb121d6a3019e0761ead89b5283ac3d/onnxruntime_gpu-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a698659271c28220b3f56fe9b63f70eae3b3c36afa544201bf750b929a36dc", size = 252761835, upload-time = "2026-03-17T22:03:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/d2/07/036825cbe30f91ea8574a18a759beccd0ea31b7b71e17f6a9ee9304b51d2/onnxruntime_gpu-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a799a16e5f1ff4d6a9e5f72d750849ab0fe534da8d323ae4a5d8d8bb7daeca8", size = 207193563, upload-time = "2026-03-17T21:58:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2c/5b3fd4748cf7ed291eae541a37e426efc20ea04cb6e6a05768304ab0aa41/onnxruntime_gpu-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb0e38f0c1ef3b76ae0081c8e51eed20dd8925aa916f0fc6f9b8b17d05610e99", size = 252765531, upload-time = "2026-03-17T22:03:57.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/86/70cecfdab1e963cc7f8c11e72040dfcd5cff85b1de2de74deba9611e0059/onnxruntime_gpu-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:da5c1e327d8e119a831be2790e69f93cf6daab9145ed0aca7577f412a620f709", size = 207197978, upload-time = "2026-03-17T21:58:38.43Z" }, + { url = "https://files.pythonhosted.org/packages/be/4e/56d11203d7a35e7d6a5ea735f5fecb8673537038c07323e8d3090a896547/onnxruntime_gpu-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbdaa73f9055fb2a177425edbed651a1843a6239f9d5430e284f4e5f65440a33", size = 252763446, upload-time = "2026-03-17T22:04:09.515Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bc/35f3a37226d7a28c84b8b456f52237ccd39eb7111114bcf9ac340178e1ec/onnxruntime_gpu-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:6be8bf2048777c517fca33eb61e114969fa326619feaa789d8c75f24337ea762", size = 207198775, upload-time = "2026-03-17T21:58:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/37/83/0c851882051b38f245f44b4a51d6232b95b8cd5d334b2c1260f2d796834f/onnxruntime_gpu-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4b348a078ced73fc577d21b83992fd2187edd10c233729c8d01b000b8543525", size = 252774594, upload-time = "2026-03-17T22:04:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5b/82b27f766b64f97c9a98b772dc07b608e900bd2faafdfa176b86d20be7f8/onnxruntime_gpu-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af9dd7ef92d94c75e5523cf070e180f3d8cdbb2fc007dcea97ba71b03e3b96d6", size = 252765395, upload-time = "2026-03-17T22:04:37.305Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/fa8c48e03790c979167d08164b34a8442c7074bca4c7253b4455497025de/onnxruntime_gpu-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:4dde3d2f1039060c42b12fd446fc0da5b836cc65dceb4020ca60a04cffa1d90d", size = 209597109, upload-time = "2026-03-17T21:58:58.136Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/7707edefcecf69d6c45b83a83f13ac58257017b4eaf58772668d302f849f/onnxruntime_gpu-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:097c6f53e99ee35f21d0fdba76ca283b92465a0e364c6f0209cb9653c424e2a4", size = 252776951, upload-time = "2026-03-17T22:04:49.715Z" }, ] [[package]] @@ -1781,70 +1806,70 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.7" +version = "3.11.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, - { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, - { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, - { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, - { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, - { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, - { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, - { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, - { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, - { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, ] [[package]] @@ -1858,98 +1883,98 @@ wheels = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pillow" -version = "12.1.1" +version = "12.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, - { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, - { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, - { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, - { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, - { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, - { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, - { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, - { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, - { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] @@ -2151,16 +2176,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -2183,7 +2208,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2192,9 +2217,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -2212,16 +2237,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -2271,11 +2296,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -2426,7 +2451,7 @@ wheels = [ [[package]] name = "rapidocr" -version = "3.6.0" +version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorlog" }, @@ -2442,7 +2467,7 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/fd/0d025466f0f84552634f2a94c018df34568fe55cc97184a6bb2c719c5b3a/rapidocr-3.6.0-py3-none-any.whl", hash = "sha256:d16b43872fc4dfa1e60996334dcd0dc3e3f1f64161e2332bc1873b9f65754e6b", size = 15067340, upload-time = "2026-01-28T14:45:04.271Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4a/fa521d947f0fc7bb304bf11bec4cb66266bd81494588b4cb48dc01001719/rapidocr-3.8.1-py3-none-any.whl", hash = "sha256:650044b1fbce9e6bae5cae462dcf8be754cde11e2f23fc51f65dcc08deae2c46", size = 15080319, upload-time = "2026-04-11T07:13:22.56Z" }, ] [[package]] @@ -2462,15 +2487,15 @@ wheels = [ [[package]] name = "rich" -version = "14.3.2" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -2536,27 +2561,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" +version = "0.15.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, ] [[package]] @@ -2909,41 +2934,41 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20250915" +version = "6.0.12.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" }, ] [[package]] name = "types-requests" -version = "2.32.4.20260107" +version = "2.33.0.20260408" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" }, ] [[package]] name = "types-setuptools" -version = "82.0.0.20260210" +version = "82.0.0.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/90/796ac8c774a7f535084aacbaa6b7053d16fff5c630eff87c3ecff7896c37/types_setuptools-82.0.0.20260210.tar.gz", hash = "sha256:d9719fbbeb185254480ade1f25327c4654f8c00efda3fec36823379cebcdee58", size = 44768, upload-time = "2026-02-10T04:22:02.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/12/3464b410c50420dd4674fa5fe9d3880711c1dbe1a06f5fe4960ee9067b9e/types_setuptools-82.0.0.20260408.tar.gz", hash = "sha256:036c68caf7e672a699f5ebbf914708d40644c14e05298bc49f7272be91cf43d3", size = 44861, upload-time = "2026-04-08T04:29:33.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/54/3489432b1d9bc713c9d8aa810296b8f5b0088403662959fb63a8acdbd4fc/types_setuptools-82.0.0.20260210-py3-none-any.whl", hash = "sha256:5124a7daf67f195c6054e0f00f1d97c69caad12fdcf9113eba33eff0bce8cd2b", size = 68433, upload-time = "2026-02-10T04:22:00.876Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/46a4fc3ef03aabf5d18bac9df5cf37c6b02c3bddf3e05c3533f4b4588331/types_setuptools-82.0.0.20260408-py3-none-any.whl", hash = "sha256:ece0a215cdfa6463a65fd6f68bd940f39e455729300ddfe61cab1147ed1d2462", size = 68428, upload-time = "2026-04-08T04:29:32.175Z" }, ] [[package]] name = "types-simplejson" -version = "3.20.0.20250822" +version = "3.20.0.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/6b/96d43a90cd202bd552cdd871858a11c138fe5ef11aeb4ed8e8dc51389257/types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0", size = 10608, upload-time = "2025-08-22T03:03:35.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/36/e319fd0f6d906dbf7c2c03eef17db77ef461197a75b253fccd9c7c695d3e/types_simplejson-3.20.0.20260408.tar.gz", hash = "sha256:0b0e1bf61e70f81dfe6ef4c2b9c02e39403848c0652df334e7a430c3a26c06b3", size = 10693, upload-time = "2026-04-08T04:28:07.8Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/9f/8e2c9e6aee9a2ff34f2ffce6ccd9c26edeef6dfd366fde611dc2e2c00ab9/types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169", size = 10417, upload-time = "2025-08-22T03:03:34.485Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/01a5a4c3948c2269cf9d727e5e66a8b404e03beb4f9522680a3f71097011/types_simplejson-3.20.0.20260408-py3-none-any.whl", hash = "sha256:f9e542199cb159ed34ad54b6ceb3dc9af890c256b810ad1bd7c69c61db7d2236", size = 10415, upload-time = "2026-04-08T04:28:06.984Z" }, ] [[package]] @@ -2987,15 +3012,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, ] [package.optional-dependencies] diff --git a/mise.toml b/mise.toml index ab44ea8652..d299bac847 100644 --- a/mise.toml +++ b/mise.toml @@ -16,8 +16,8 @@ config_roots = [ [tools] node = "24.14.1" flutter = "3.35.7" -pnpm = "10.32.1" -terragrunt = "0.99.5" +pnpm = "10.33.0" +terragrunt = "1.0.0" opentofu = "1.11.5" java = "21.0.2" diff --git a/mobile/.isar b/mobile/.isar deleted file mode 160000 index 6643d064ab..0000000000 --- a/mobile/.isar +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6643d064abf22606b6c6a741ea873e4781115ef4 diff --git a/mobile/.isar-cargo.lock b/mobile/.isar-cargo.lock deleted file mode 100644 index a7b1dd37b9..0000000000 --- a/mobile/.isar-cargo.lock +++ /dev/null @@ -1,859 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "bindgen" -version = "0.63.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 1.0.109", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" - -[[package]] -name = "cc" -version = "1.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" -dependencies = [ - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "cmake" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" -dependencies = [ - "cc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "float_next_after" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632" -dependencies = [ - "num-traits", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "intmap" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee87fd093563344074bacf24faa0bb0227fb6969fb223e922db798516de924d6" - -[[package]] -name = "isar" -version = "0.0.0" -dependencies = [ - "dirs", - "intmap", - "isar-core", - "itertools", - "jni", - "ndk-context", - "objc", - "objc-foundation", - "once_cell", - "paste", - "serde_json", - "threadpool", - "unicode-segmentation", -] - -[[package]] -name = "isar-core" -version = "0.0.0" -dependencies = [ - "byteorder", - "cfg-if", - "crossbeam-channel", - "enum_dispatch", - "float_next_after", - "intmap", - "itertools", - "libc", - "mdbx-sys", - "once_cell", - "paste", - "rand", - "serde", - "serde_json", - "snafu", - "widestring", - "xxhash-rust", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "jni" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "libloading" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.6.0", - "libc", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "mdbx-sys" -version = "0.0.0" -dependencies = [ - "bindgen", - "cc", - "cmake", - "libc", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "once_cell" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.210" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.210" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "serde_json" -version = "1.0.128" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "snafu" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "widestring" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "xxhash-rust" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index eafbef8102..1bb94819e5 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.35.7", + "dart.flutterSdkPath": ".fvm/versions/3.41.6", "dart.lineLength": 120, "[dart]": { "editor.rulers": [ diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 895203fb98..fafd1f40ec 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -52,90 +52,11 @@ analyzer: unawaited_futures: warning custom_lint: - debug: true rules: - avoid_build_context_in_providers: false - avoid_public_notifier_properties: false - avoid_manual_providers_as_generated_provider_dependency: false - unsupported_provider_value: false - - import_rule_photo_manager: - message: photo_manager must only be used in MediaRepositories - restrict: package:photo_manager - allowed: - # required / wanted - - 'lib/infrastructure/repositories/album_media.repository.dart' - - 'lib/infrastructure/repositories/{storage,asset_media}.repository.dart' - - 'lib/repositories/{album,asset,file}_media.repository.dart' - # acceptable exceptions for the time being - - lib/entities/asset.entity.dart # to provide local AssetEntity for now - - lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager - # refactor to make the providers and services testable - - lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler - - lib/services/{background,backup}.service.dart # uses only PMProgressHandler - - test/**.dart - - import_rule_isar: - message: isar must only be used in entities and repositories - restrict: package:isar - allowed: - # required / wanted - - lib/entities/*.entity.dart - - lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart - - lib/infrastructure/entities/*.entity.dart - - lib/infrastructure/repositories/*.repository.dart - - lib/providers/infrastructure/db.provider.dart - # acceptable exceptions for the time being (until Isar is fully replaced) - - lib/providers/app_life_cycle.provider.dart - - integration_test/test_utils/general_helper.dart - - lib/domain/services/background_worker.service.dart - - lib/main.dart - - lib/pages/album/album_asset_selection.page.dart - - lib/routing/router.dart - - lib/services/immich_logger.service.dart # not really a service... more a util - - lib/utils/{db,migration}.dart - - lib/utils/bootstrap.dart - - lib/widgets/asset_grid/asset_grid_data_structure.dart - - test/**.dart - # refactor the remaining providers - - lib/providers/db.provider.dart - - - import_rule_openapi: - message: openapi must only be used through ApiRepositories - restrict: package:openapi - allowed: - # required / wanted - - lib/repositories/*_api.repository.dart - - lib/domain/models/sync_event.model.dart - - lib/{domain,infrastructure}/**/sync_stream.* - - lib/{domain,infrastructure}/**/sync_api.* - - lib/infrastructure/repositories/*_api.repository.dart - - lib/infrastructure/utils/*.converter.dart - # acceptable exceptions for the time being - - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities - - lib/infrastructure/utils/*.converter.dart - - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine - - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... - - lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database - - lib/domain/services/search.service.dart - - # refactor - - lib/models/map/map_marker.model.dart - - lib/models/server_info/server_{config,disk_info,features,version}.model.dart - - lib/models/shared_link/shared_link.model.dart - - lib/providers/asset_viewer/asset_people.provider.dart - - lib/providers/auth.provider.dart - - lib/providers/image/immich_remote_{image,thumbnail}_provider.dart - - lib/providers/map/map_state.provider.dart - - lib/providers/search/{search,search_filter}.provider.dart - - lib/providers/websocket.provider.dart - - lib/routing/auth_guard.dart - - lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.service.dart - - lib/widgets/album/album_thumbnail_listtile.dart - - lib/widgets/forms/login/login_form.dart - - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart - - lib/services/auth.service.dart # on ApiException - - test/services/auth.service_test.dart # on ApiException - # allow import from test - - test/**.dart dart_code_metrics: rules: diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index ecc0f8420f..e879b54ae5 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,27 +1,10 @@ plugins { - id "com.android.application" - id "kotlin-android" + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) id "dev.flutter.flutter-gradle-plugin" - id 'com.google.devtools.ksp' - id 'org.jetbrains.kotlin.plugin.serialization' - id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version - -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withInputStream { localProperties.load(it) } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.compose) } def keystoreProperties = new Properties() @@ -31,8 +14,8 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 35 - ndkVersion = "28.2.13676358" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -55,10 +38,10 @@ android { defaultConfig { applicationId "app.alextran.immich" - minSdkVersion 26 - targetSdkVersion 35 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + minSdk = 26 + targetSdk = flutter.targetSdkVersion + versionCode flutter.versionCode + versionName flutter.versionName } signingConfigs { @@ -67,10 +50,10 @@ android { def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") - keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] - keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] - storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) - storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] + keyAlias keyAliasVal ?: keystoreProperties['keyAlias'] + keyPassword keyPasswordVal ?: keystoreProperties['keyPassword'] + storeFile file("../key.jks").exists() ? file("../key.jks") : file(keystoreProperties['storeFile'] ?: '../key.jks') + storePassword storePasswordVal ?: keystoreProperties['storePassword'] } } @@ -99,43 +82,31 @@ flutter { } dependencies { - def kotlin_version = '2.0.20' - def kotlin_coroutines_version = '1.9.0' - def work_version = '2.9.1' - def concurrent_version = '1.2.0' - def guava_version = '33.3.1-android' - def glide_version = '4.16.0' - def serialization_version = '1.8.1' - def compose_version = '1.1.1' - def gson_version = '2.10.1' - def okhttp_version = '4.12.0' + implementation libs.okhttp + implementation libs.cronet.embedded + implementation libs.media3.datasource.okhttp + implementation libs.media3.datasource.cronet + implementation libs.kotlinx.coroutines.android + implementation libs.work.runtime.ktx + implementation libs.concurrent.futures + implementation libs.guava + implementation libs.glide + implementation libs.kotlinx.serialization.json - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "com.squareup.okhttp3:okhttp:$okhttp_version" - implementation 'org.chromium.net:cronet-embedded:143.7445.0' - implementation("androidx.media3:media3-datasource-okhttp:1.10.0") - implementation("androidx.media3:media3-datasource-cronet:1.10.0") - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.work:work-runtime-ktx:$work_version" - implementation "androidx.concurrent:concurrent-futures:$concurrent_version" - implementation "com.google.guava:guava:$guava_version" - implementation "com.github.bumptech.glide:glide:$glide_version" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" - - ksp "com.github.bumptech.glide:ksp:$glide_version" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + ksp libs.glide.ksp + coreLibraryDesugaring libs.desugar.jdk.libs //Glance Widget - implementation "androidx.glance:glance-appwidget:$compose_version" - implementation "com.google.code.gson:gson:$gson_version" + implementation libs.glance.appwidget + implementation libs.gson // Glance Configure - implementation "androidx.activity:activity-compose:1.8.2" - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.ui:ui-tooling:$compose_version" - implementation "androidx.compose.material3:material3:1.2.1" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" - implementation "com.google.android.material:material:1.12.0" + implementation libs.activity.compose + implementation libs.compose.ui + implementation libs.compose.ui.tooling + implementation libs.compose.material3 + implementation libs.lifecycle.runtime.ktx + implementation libs.material } // This is uncommented in F-Droid build script diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index db3859ab6e..436d8c492d 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -39,10 +39,6 @@ android:exported="false" android:foregroundServiceType="dataSync|shortService" /> - - diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt deleted file mode 100644 index f62f25558d..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt +++ /dev/null @@ -1,389 +0,0 @@ -package app.alextran.immich - -import android.app.Activity -import android.content.ContentResolver -import android.content.ContentUris -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.provider.Settings -import android.util.Log -import androidx.annotation.RequiresApi -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry -import java.security.MessageDigest -import java.io.FileInputStream -import kotlinx.coroutines.* -import androidx.core.net.toUri - -/** - * Android plugin for Dart `BackgroundService` and file trash operations - */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { - - private var methodChannel: MethodChannel? = null - private var fileTrashChannel: MethodChannel? = null - private var context: Context? = null - private var pendingResult: Result? = null - private val permissionRequestCode = 1001 - private val trashRequestCode = 1002 - private var activityBinding: ActivityPluginBinding? = null - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) - } - - private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { - context = ctx - methodChannel = MethodChannel(messenger, "immich/foregroundChannel") - methodChannel?.setMethodCallHandler(this) - - // Add file trash channel - fileTrashChannel = MethodChannel(messenger, "file_trash") - fileTrashChannel?.setMethodCallHandler(this) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() - } - - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - fileTrashChannel?.setMethodCallHandler(null) - fileTrashChannel = null - } - - override fun onMethodCall(call: MethodCall, result: Result) { - val ctx = context!! - when (call.method) { - // Existing BackgroundService methods - "enable" -> { - val args = call.arguments>()!! - ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) - .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long) - .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String) - .apply() - ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean) - result.success(true) - } - - "configure" -> { - val args = call.arguments>()!! - val requireUnmeteredNetwork = args[0] as Boolean - val requireCharging = args[1] as Boolean - val triggerUpdateDelay = (args[2] as Number).toLong() - val triggerMaxDelay = (args[3] as Number).toLong() - ContentObserverWorker.configureWork( - ctx, - requireUnmeteredNetwork, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay - ) - result.success(true) - } - - "disable" -> { - ContentObserverWorker.disable(ctx) - BackupWorker.stopWork(ctx) - result.success(true) - } - - "isEnabled" -> { - result.success(ContentObserverWorker.isEnabled(ctx)) - } - - "isIgnoringBatteryOptimizations" -> { - result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx)) - } - - "digestFiles" -> { - val args = call.arguments>()!! - GlobalScope.launch(Dispatchers.IO) { - val buf = ByteArray(BUFFER_SIZE) - val digest: MessageDigest = MessageDigest.getInstance("SHA-1") - val hashes = arrayOfNulls(args.size) - for (i in args.indices) { - val path = args[i] - var len = 0 - try { - val file = FileInputStream(path) - file.use { assetFile -> - while (true) { - len = assetFile.read(buf) - if (len != BUFFER_SIZE) break - digest.update(buf) - } - } - digest.update(buf, 0, len) - hashes[i] = digest.digest() - } catch (e: Exception) { - // skip this file - Log.w(TAG, "Failed to hash file ${args[i]}: $e") - } - } - result.success(hashes.asList()) - } - } - - // File Trash methods moved from MainActivity - "moveToTrash" -> { - val mediaUrls = call.argument>("mediaUrls") - if (mediaUrls != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - moveToTrash(mediaUrls, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_NAME", "The mediaUrls is not specified.", null) - } - } - - "restoreFromTrash" -> { - val fileName = call.argument("fileName") - val type = call.argument("type") - val mediaId = call.argument("mediaId") - if (fileName != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrash(fileName, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else - if (mediaId != null && type != null) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { - restoreFromTrashById(mediaId, type, result) - } else { - result.error("PERMISSION_DENIED", "Media permission required", null) - } - } else { - result.error("INVALID_PARAMS", "Required params are not specified.", null) - } - } - - "requestManageMediaPermission" -> { - if (!hasManageMediaPermission()) { - requestManageMediaPermission(result) - } else { - Log.e("Manage storage permission", "Permission already granted") - result.success(true) - } - } - - "hasManageMediaPermission" -> { - if (hasManageMediaPermission()) { - Log.i("Manage storage permission", "Permission already granted") - result.success(true) - } else { - result.success(false) - } - } - - "manageMediaPermission" -> requestManageMediaPermission(result) - - else -> result.notImplemented() - } - } - - private fun hasManageMediaPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MediaStore.canManageMedia(context!!); - } else { - false - } - } - - private fun requestManageMediaPermission(result: Result) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - pendingResult = result // Store the result callback - val activity = activityBinding?.activity ?: return - - val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) - intent.data = "package:${activity.packageName}".toUri() - activity.startActivityForResult(intent, permissionRequestCode) - } else { - result.success(false) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun moveToTrash(mediaUrls: List, result: Result) { - val urisToTrash = mediaUrls.map { it.toUri() } - if (urisToTrash.isEmpty()) { - result.error("INVALID_ARGS", "No valid URIs provided", null) - return - } - - toggleTrash(urisToTrash, true, result); - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrash(name: String, type: Int, result: Result) { - val uri = getTrashedFileUri(name, type) - if (uri == null) { - Log.e("TrashError", "Asset Uri cannot be found obtained") - result.error("TrashError", "Asset Uri cannot be found obtained", null) - return - } - Log.e("FILE_URI", uri.toString()) - uri.let { toggleTrash(listOf(it), false, result) } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreFromTrashById(mediaId: String, type: Int, result: Result) { - val id = mediaId.toLongOrNull() - if (id == null) { - result.error("INVALID_ID", "The file id is not a valid number: $mediaId", null) - return - } - if (!isInTrash(id)) { - result.error("TrashNotFound", "Item with id=$id not found in trash", null) - return - } - - val uri = ContentUris.withAppendedId(contentUriForType(type), id) - - try { - Log.i(TAG, "restoreFromTrashById: uri=$uri (type=$type,id=$id)") - restoreUris(listOf(uri), result) - } catch (e: Exception) { - Log.w(TAG, "restoreFromTrashById failed", e) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun toggleTrash(contentUris: List, isTrashed: Boolean, result: Result) { - val activity = activityBinding?.activity - val contentResolver = context?.contentResolver - if (activity == null || contentResolver == null) { - result.error("TrashError", "Activity or ContentResolver not available", null) - return - } - - try { - val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed) - pendingResult = result // Store for onActivityResult - activity.startIntentSenderForResult( - pendingIntent.intentSender, - trashRequestCode, - null, 0, 0, 0 - ) - } catch (e: Exception) { - Log.e("TrashError", "Error creating or starting trash request", e) - result.error("TrashError", "Error creating or starting trash request", null) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun getTrashedFileUri(fileName: String, type: Int): Uri? { - val contentResolver = context?.contentResolver ?: return null - val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val projection = arrayOf(MediaStore.Files.FileColumns._ID) - - val queryArgs = Bundle().apply { - putString( - ContentResolver.QUERY_ARG_SQL_SELECTION, - "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?" - ) - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - } - - contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) - return ContentUris.withAppendedId(contentUriForType(type), id) - } - } - return null - } - - // ActivityAware implementation - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivity() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - // ActivityResultListener implementation - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == permissionRequestCode) { - val granted = hasManageMediaPermission() - pendingResult?.success(granted) - pendingResult = null - return true - } - - if (requestCode == trashRequestCode) { - val approved = resultCode == Activity.RESULT_OK - pendingResult?.success(approved) - pendingResult = null - return true - } - return false - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun isInTrash(id: Long): Boolean { - val contentResolver = context?.contentResolver ?: return false - val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val args = Bundle().apply { - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?") - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString())) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - putInt(ContentResolver.QUERY_ARG_LIMIT, 1) - } - return contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null) - ?.use { it.moveToFirst() } == true - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun restoreUris(uris: List, result: Result) { - if (uris.isEmpty()) { - result.error("TrashError", "No URIs to restore", null) - return - } - Log.i(TAG, "restoreUris: count=${uris.size}, first=${uris.first()}") - toggleTrash(uris, false, result) - } - - @RequiresApi(Build.VERSION_CODES.Q) - private fun contentUriForType(type: Int): Uri = - when (type) { - // same order as AssetType from dart - 1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - 2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - 3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - } -} - -private const val TAG = "BackgroundServicePlugin" -private const val BUFFER_SIZE = 2 * 1024 * 1024 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt deleted file mode 100644 index 9c90528dc9..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt +++ /dev/null @@ -1,394 +0,0 @@ -package app.alextran.immich - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.os.PowerManager -import android.os.SystemClock -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.concurrent.futures.ResolvableFuture -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ForegroundInfo -import androidx.work.ListenableWorker -import androidx.work.NetworkType -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.WorkInfo -import com.google.common.util.concurrent.ListenableFuture -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor -import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.view.FlutterCallbackInformation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager to perform backup in background - * - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - * Called by Android WorkManager when all constraints for the work are met, - * i.e. battery is not low and optionally Wifi and charging are active. - */ -class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), - MethodChannel.MethodCallHandler { - - private val resolvableFuture = ResolvableFuture.create() - private var engine: FlutterEngine? = null - private lateinit var backgroundChannel: MethodChannel - private val notificationManager = - ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) - private var timeBackupStarted: Long = 0L - private var notificationBuilder: NotificationCompat.Builder? = null - private var notificationDetailBuilder: NotificationCompat.Builder? = null - private var fgFuture: ListenableFuture? = null - - override fun startWork(): ListenableFuture { - - Log.d(TAG, "startWork") - - val ctx = applicationContext - - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) - } - - // Create a Notification channel - createChannel() - - Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations") - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate = true).build()) - } - - engine = FlutterEngine(ctx) - - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - - } - - return resolvableFuture - } - - /** - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. - */ - private fun runDart() { - val callbackDispatcherHandle = applicationContext.getSharedPreferences( - SHARED_PREF_NAME, Context.MODE_PRIVATE - ).getLong(SHARED_PREF_CALLBACK_KEY, 0L) - val callbackInformation = - FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle) - val appBundlePath = flutterLoader.findAppBundlePath() - - engine?.let { engine -> - backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel") - backgroundChannel.setMethodCallHandler(this@BackupWorker) - engine.dartExecutor.executeDartCallback( - DartExecutor.DartCallback( - applicationContext.assets, - appBundlePath, - callbackInformation - ) - ) - } - } - - override fun onStopped() { - Log.d(TAG, "onStopped") - // called when the system has to stop this worker because constraints are - // no longer met or the system needs resources for more important tasks - Handler(Looper.getMainLooper()).postAtFrontOfQueue { - if (::backgroundChannel.isInitialized) { - backgroundChannel.invokeMethod("systemStop", null) - } - } - waitOnSetForegroundAsync() - // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) - // instead, wait for 5 seconds until forcefully stopping backup work - Handler(Looper.getMainLooper()).postDelayed({ - stopEngine(null) - }, 5000) - } - - private fun waitOnSetForegroundAsync() { - val fgFuture = this.fgFuture - if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) { - try { - fgFuture.get(500, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - // ignored, there is nothing to be done - } - } - } - - private fun stopEngine(result: Result?) { - clearBackgroundNotification() - engine?.destroy() - engine = null - if (result != null) { - Log.d(TAG, "stopEngine result=${result}") - resolvableFuture.set(result) - } - waitOnSetForegroundAsync() - } - - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - timeBackupStarted = SystemClock.uptimeMillis() - backgroundChannel.invokeMethod( - "onAssetsChanged", - null, - object : MethodChannel.Result { - override fun notImplemented() { - stopEngine(Result.failure()) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - stopEngine(Result.failure()) - } - - override fun success(receivedResult: Any?) { - val success = receivedResult as Boolean - stopEngine(if (success) Result.success() else Result.retry()) - } - } - ) - } - - "updateNotification" -> { - val args = call.arguments>()!! - val title = args[0] as String? - val content = args[1] as String? - val progress = args[2] as Int - val max = args[3] as Int - val indeterminate = args[4] as Boolean - val isDetail = args[5] as Boolean - val onlyIfFG = args[6] as Boolean - if (!onlyIfFG || isIgnoringBatteryOptimizations) { - showInfo( - getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), - isDetail - ) - } - } - - "showError" -> { - val args = call.arguments>()!! - val title = args[0] as String - val content = args[1] as String? - val individualTag = args[2] as String? - showError(title, content, individualTag) - } - - "clearErrorNotifications" -> clearErrorNotifications() - "hasContentChanged" -> { - val lastChange = applicationContext - .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted) - val hasContentChanged = lastChange > timeBackupStarted; - timeBackupStarted = SystemClock.uptimeMillis() - r.success(hasContentChanged) - } - - else -> r.notImplemented() - } - } - - private fun showError(title: String, content: String?, individualTag: String?) { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.drawable.notification_icon) - .build() - notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) - } - - private fun clearErrorNotifications() { - notificationManager.cancel(NOTIFICATION_ERROR_ID) - } - - private fun clearBackgroundNotification() { - notificationManager.cancel(NOTIFICATION_ID) - notificationManager.cancel(NOTIFICATION_DETAIL_ID) - } - - private fun showInfo(notification: Notification, isDetail: Boolean = false) { - val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID - - if (isIgnoringBatteryOptimizations && !isDetail) { - fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)) - } else { - setForegroundAsync(ForegroundInfo(id, notification)) - } - } else { - notificationManager.notify(id, notification) - } - } - - private fun getInfoBuilder( - title: String? = null, - content: String? = null, - isDetail: Boolean = false, - progress: Int = 0, - max: Int = 0, - indeterminate: Boolean = false, - ): NotificationCompat.Builder { - var builder = if (isDetail) notificationDetailBuilder else notificationBuilder - if (builder == null) { - builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon) - .setOnlyAlertOnce(true) - .setOngoing(true) - if (isDetail) { - notificationDetailBuilder = builder - } else { - notificationBuilder = builder - } - } - if (title != null) { - builder.setTicker(title).setContentTitle(title) - } - if (content != null) { - builder.setContentText(content) - } - return builder.setProgress(max, progress, indeterminate) - } - - private fun createChannel() { - val foreground = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_ID, - NotificationManager.IMPORTANCE_LOW - ) - notificationManager.createNotificationChannel(foreground) - val error = NotificationChannel( - NOTIFICATION_CHANNEL_ERROR_ID, - NOTIFICATION_CHANNEL_ERROR_ID, - NotificationManager.IMPORTANCE_HIGH - ) - notificationManager.createNotificationChannel(error) - } - - companion object { - const val SHARED_PREF_NAME = "immichBackgroundService" - const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" - const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" - const val SHARED_PREF_LAST_CHANGE = "lastChange" - - private const val TASK_NAME_BACKUP = "immich/BackupWorker" - private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" - private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" - private const val NOTIFICATION_DEFAULT_TITLE = "Immich" - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 - private const val NOTIFICATION_DETAIL_ID = 3 - private const val ONE_MINUTE = 60000L - - /** - * Enqueues the BackupWorker to run once the constraints are met - */ - fun enqueueBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ) { - val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds) - WorkManager.getInstance(context) - .enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest) - Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued") - } - - /** - * Updates the constraints of an already enqueued BackupWorker - */ - fun updateBackupWorker( - context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false - ) { - try { - val wm = WorkManager.getInstance(context) - val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP) - val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) - if (workInfoList != null) { - for (workInfo in workInfoList) { - if (workInfo.state == WorkInfo.State.ENQUEUED) { - val workRequest = buildWorkRequest(requireWifi, requireCharging) - wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) - Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") - return - } - } - } - Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued") - } catch (e: Exception) { - Log.d(TAG, "updateBackupWorker failed: $e") - } - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun stopWork(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP) - Log.d(TAG, "stopWork: BackupWorker cancelled") - } - - /** - * Returns `true` if the app is ignoring battery optimizations - */ - fun isIgnoringBatteryOptimizations(ctx: Context): Boolean { - val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager - return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) - } - - private fun buildWorkRequest( - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L - ): OneTimeWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .setRequiresCharging(requireCharging) - .build(); - - val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) - .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS) - .build() - return work - } - - private val flutterLoader = FlutterLoader() - } -} - -private const val TAG = "BackupWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt deleted file mode 100644 index 9cb2ec7779..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt +++ /dev/null @@ -1,144 +0,0 @@ -package app.alextran.immich - -import android.content.Context -import android.os.SystemClock -import android.provider.MediaStore -import android.util.Log -import androidx.work.Constraints -import androidx.work.Worker -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.Operation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager observing content changes (new photos/videos) - * - * Immediately enqueues the BackupWorker when running. - * As this work is not triggered periodically, but on content change, the - * worker enqueues itself again after each run. - */ -class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { - - override fun doWork(): Result { - if (!isEnabled(applicationContext)) { - return Result.failure() - } - if (triggeredContentUris.size > 0) { - startBackupWorker(applicationContext, delayMilliseconds = 0) - } - enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE) - return Result.success() - } - - companion object { - const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled" - private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi" - private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging" - private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay" - private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay" - - private const val TASK_NAME_OBSERVER = "immich/ContentObserver" - - /** - * Enqueues the `ContentObserverWorker`. - * - * @param context Android Context - */ - fun enable(context: Context, immediate: Boolean = false) { - enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) - Log.d(TAG, "enabled ContentObserverWorker") - if (immediate) { - startBackupWorker(context, delayMilliseconds = 5000) - } - } - - /** - * Configures the `BackupWorker` to run when all constraints are met. - * - * @param context Android Context - * @param requireWifi if true, task only runs if connected to wifi - * @param requireCharging if true, task only runs if device is charging - */ - fun configureWork(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - triggerUpdateDelay: Long = 5000, - triggerMaxDelay: Long = 50000) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(SHARED_PREF_SERVICE_ENABLED, true) - .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi) - .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging) - .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay) - .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay) - .apply() - BackupWorker.updateBackupWorker(context, requireWifi, requireCharging) - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun disable(context: Context) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply() - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER) - Log.d(TAG, "disabled ContentObserverWorker") - } - - /** - * Return true if the user has enabled the background backup service - */ - fun isEnabled(ctx: Context): Boolean { - return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(SHARED_PREF_SERVICE_ENABLED, false) - } - - /** - * Enqueue and replace the worker without the content trigger but with a short delay - */ - fun workManagerAppClearedWorkaround(context: Context) { - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setInitialDelay(500, TimeUnit.MILLISECONDS) - .build() - WorkManager - .getInstance(context) - .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work) - .result - .get() - Log.d(TAG, "workManagerAppClearedWorkaround") - } - - private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - val constraints = Constraints.Builder() - .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) - .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS) - .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS) - .build() - - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setConstraints(constraints) - .build() - WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) - } - - fun startBackupWorker(context: Context, delayMilliseconds: Long) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) - return - val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) - val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) - BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) - sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply() - } - - } -} - -private const val TAG = "ContentObserverWorker" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt index 4474c63e09..37a325e896 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt @@ -18,8 +18,6 @@ class ImmichApp : Application() { // Thus, the BackupWorker is not started. If the system kills the process after each initialization // (because of low memory etc.), the backup is never performed. // As a workaround, we also run a backup check when initializing the application - - ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) Handler(Looper.getMainLooper()).postDelayed({ // We can only check the engine count and not the status of the lock here, // as the previous start might have been killed without unlocking. diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 06649de8f0..2c80b8d2bd 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -51,7 +51,6 @@ class MainActivity : FlutterFragmentActivity() { BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) - flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(nativeSyncApiImpl) } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index 05671579ae..eea66db2f6 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -94,11 +94,12 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { const val HASH_BUFFER_SIZE = 2 * 1024 * 1024 - // _special_format requires S Extensions 21+ + // _special_format: added in API level 37, also in S Extensions 21+ // https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT private fun hasSpecialFormatColumn(): Boolean = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 21 + Build.VERSION.SDK_INT >= 37 || + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 21) } protected fun getCursor( diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 719c946bd6..2663154d9d 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,6 +1,4 @@ allprojects { - ext.kotlin_version = '2.2.20' - repositories { google() mavenCentral() @@ -10,22 +8,7 @@ allprojects { rootProject.buildDir = '../build' subprojects { - // fix for verifyReleaseResources - // ============ - afterEvaluate { project -> - if (project.plugins.hasPlugin("com.android.application") || - project.plugins.hasPlugin("com.android.library")) { - project.android { - compileSdkVersion 36 - buildToolsVersion "36.0.0" - } - } - } - // ============ project.buildDir = "${rootProject.buildDir}/${project.name}" -} - -subprojects { project.evaluationDependsOn(':app') } @@ -36,4 +19,3 @@ tasks.register("clean", Delete) { tasks.named('wrapper') { distributionType = Wrapper.DistributionType.ALL } - diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index be1a17d03d..7312a8ca68 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 3045, - "android.injected.version.name" => "2.7.4", + "android.injected.version.code" => 3046, + "android.injected.version.name" => "2.7.5", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/gradle/libs.versions.toml b/mobile/android/gradle/libs.versions.toml new file mode 100644 index 0000000000..0161433c05 --- /dev/null +++ b/mobile/android/gradle/libs.versions.toml @@ -0,0 +1,51 @@ +[versions] +agp = "8.11.2" +kotlin = "2.2.20" +ksp = "2.2.20-2.0.3" +coroutines = "1.9.0" +work = "2.9.1" +concurrent = "1.2.0" +guava = "33.3.1-android" +glide = "4.16.0" +serialization-json = "1.8.1" +glance = "1.1.1" +gson = "2.10.1" +okhttp = "4.12.0" +cronet = "143.7445.0" +media3 = "1.10.0" +desugar = "2.1.2" +activity-compose = "1.8.2" +compose-ui = "1.1.1" +material3 = "1.2.1" +lifecycle = "2.6.2" +material = "1.12.0" + +[libraries] +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +cronet-embedded = { module = "org.chromium.net:cronet-embedded", version.ref = "cronet" } +media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" } +media3-datasource-cronet = { module = "androidx.media3:media3-datasource-cronet", version.ref = "media3" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } +concurrent-futures = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrent" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization-json" } +desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } +glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-ui" } +compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } +lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +material = { module = "com.google.android.material:material", version.ref = "material" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +# TODO: update to version.ref = "kotlin" when background_downloader is removed +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "1.9.22" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties index ed4c299adb..6514f919fd 100644 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index fbed55a3e3..664cb9c263 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -18,10 +18,11 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.11.2' apply false + id "com.android.application" version "8.11.2" apply false id "org.jetbrains.kotlin.android" version "2.2.20" apply false - id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false - id 'com.google.devtools.ksp' version '2.2.20-2.0.3' apply false + // TODO: update to match kotlin version when background_downloader is removed + id "org.jetbrains.kotlin.plugin.serialization" version "1.9.22" apply false + id "com.google.devtools.ksp" version "2.2.20-2.0.3" apply false } include ":app" diff --git a/mobile/dart_test.yaml b/mobile/dart_test.yaml deleted file mode 100644 index fa54954090..0000000000 --- a/mobile/dart_test.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# Used to filter out tags from test runs -tags: - widget: diff --git a/mobile/immich_lint/analysis_options.yaml b/mobile/immich_lint/analysis_options.yaml deleted file mode 100644 index 572dd239d0..0000000000 --- a/mobile/immich_lint/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:lints/recommended.yaml diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart deleted file mode 100644 index 7d3ed4757e..0000000000 --- a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:analyzer/error/error.dart' show ErrorSeverity; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -// ignore: depend_on_referenced_packages -import 'package:glob/glob.dart'; - -PluginBase createPlugin() => ImmichLinter(); - -class ImmichLinter extends PluginBase { - @override - List getLintRules(CustomLintConfigs configs) { - final List rules = []; - for (final entry in configs.rules.entries) { - if (entry.value.enabled && entry.key.startsWith("import_rule_")) { - final code = makeCode(entry.key, entry.value); - final allowedPaths = getStrings(entry.value, "allowed"); - final forbiddenPaths = getStrings(entry.value, "forbidden"); - final restrict = getStrings(entry.value, "restrict"); - rules.add(ImportRule(code, buildGlob(allowedPaths), - buildGlob(forbiddenPaths), restrict)); - } - } - return rules; - } - - static LintCode makeCode(String name, LintOptions options) => LintCode( - name: name, - problemMessage: options.json["message"] as String, - errorSeverity: ErrorSeverity.WARNING, - ); - - static List getStrings(LintOptions options, String field) { - final List result = []; - final excludeOption = options.json[field]; - if (excludeOption is String) { - result.add(excludeOption); - } else if (excludeOption is List) { - result.addAll(excludeOption.map((option) => option)); - } - return result; - } - - Glob? buildGlob(List globs) { - if (globs.isEmpty) return null; - if (globs.length == 1) return Glob(globs[0], caseSensitive: true); - return Glob("{${globs.join(",")}}", caseSensitive: true); - } -} - -// ignore: must_be_immutable -class ImportRule extends DartLintRule { - ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict) - : super(code: code); - - final Glob? _allowed; - final Glob? _forbidden; - final List _restrict; - int _rootOffset = -1; - - @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, - ) { - if (_rootOffset == -1) { - const project = "/immich/mobile/"; - _rootOffset = - resolver.path.toLowerCase().indexOf(project) + project.length; - } - final path = resolver.path.substring(_rootOffset); - - if ((_allowed != null && _allowed!.matches(path)) && - (_forbidden == null || !_forbidden!.matches(path))) { - return; - } - - context.registry.addImportDirective((node) { - final uri = node.uri.stringValue; - if (uri == null) return; - for (final restricted in _restrict) { - if (uri.startsWith(restricted) == true) { - reporter.atNode(node, code); - return; - } - } - }); - } -} diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock deleted file mode 100644 index 0e4b08be87..0000000000 --- a/mobile/immich_lint/pubspec.lock +++ /dev/null @@ -1,365 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f - url: "https://pub.dev" - source: hosted - version: "82.0.0" - analyzer: - dependency: "direct main" - description: - name: analyzer - sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" - url: "https://pub.dev" - source: hosted - version: "7.4.5" - analyzer_plugin: - dependency: "direct main" - description: - name: analyzer_plugin - sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef - url: "https://pub.dev" - source: hosted - version: "0.13.1" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - custom_lint: - dependency: transitive - description: - name: custom_lint - sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_builder: - dependency: "direct main" - description: - name: custom_lint_builder - sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_visitor: - dependency: transitive - description: - name: custom_lint_visitor - sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d - url: "https://pub.dev" - source: hosted - version: "1.0.0+7.4.5" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - glob: - dependency: "direct main" - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - hotreloader: - dependency: transitive - description: - name: hotreloader - sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b - url: "https://pub.dev" - source: hosted - version: "4.3.0" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - lints: - dependency: "direct dev" - description: - name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 - url: "https://pub.dev" - source: hosted - version: "6.0.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" - url: "https://pub.dev" - source: hosted - version: "0.7.6" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.8.0 <4.0.0" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml deleted file mode 100644 index e49e9c5010..0000000000 --- a/mobile/immich_lint/pubspec.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: immich_mobile_immich_lint -publish_to: none - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - analyzer: ^7.0.0 - analyzer_plugin: ^0.13.0 - custom_lint_builder: ^0.7.5 - glob: ^2.1.2 - -dev_dependencies: - lints: ^6.0.0 diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index d6065170ef..66955364f3 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/main.dart' as app; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:integration_test/integration_test.dart'; @@ -39,20 +38,11 @@ class ImmichTestHelper { static Future loadApp(WidgetTester tester) async { await EasyLocalization.ensureInitialized(); // Clear all data from Isar (reuse existing instance if available) - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await Store.clear(); - await isar.writeTxn(() => isar.clear()); // Load main Widget await tester.pumpWidget( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const app.MainWidget(), - ), + ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const app.MainWidget()), ); // Post run tasks await EasyLocalization.ensureInitialized(); diff --git a/mobile/ios/Flutter/AppFrameworkInfo.plist b/mobile/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7652..391a902b2b 100644 --- a/mobile/ios/Flutter/AppFrameworkInfo.plist +++ b/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e1ec4aff07..c0d7e2c35a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -33,8 +33,6 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - isar_community_flutter_libs (1.0.0): - - Flutter - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -75,16 +73,16 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/perf-threadsafe (3.49.2): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter @@ -116,7 +114,6 @@ DEPENDENCIES: - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) @@ -174,8 +171,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - isar_community_flutter_libs: - :path: ".symlinks/plugins/isar_community_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" maplibre_gl: @@ -228,7 +223,6 @@ SPEC CHECKSUMS: home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f @@ -245,7 +239,7 @@ SPEC CHECKSUMS: share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 178454f381..f88d624b89 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; }; - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -90,8 +88,6 @@ 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; }; - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -151,11 +147,15 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B231F52D2E93A44A00BC45D1 /* Core */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Core; sourceTree = ""; }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Sync; sourceTree = ""; }; @@ -177,6 +177,8 @@ }; FEE084F22EC172080045228E /* Schemas */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Schemas; sourceTree = ""; }; @@ -238,15 +240,6 @@ name = Frameworks; sourceTree = ""; }; - 65DD438629917FAD0047FFA8 /* BackgroundSync */ = { - isa = PBXGroup; - children = ( - 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */, - 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */, - ); - path = BackgroundSync; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -291,7 +284,6 @@ B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, - 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -571,14 +563,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -607,14 +595,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -627,7 +611,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, @@ -642,7 +625,6 @@ B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, - 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1261,7 +1243,7 @@ repositoryURL = "https://github.com/pointfreeco/sqlite-data"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.6.1; }; }; FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = { @@ -1269,7 +1251,7 @@ repositoryURL = "https://github.com/apple/swift-http-structured-headers.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.5.0; + minimumVersion = 1.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 432e81234d..187a67cb27 100644 --- a/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,31 +19,13 @@ "version" : "7.8.0" } }, - { - "identity" : "opencombine", - "kind" : "remoteSourceControl", - "location" : "https://github.com/OpenCombine/OpenCombine.git", - "state" : { - "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", - "version" : "0.14.0" - } - }, { "identity" : "sqlite-data", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "b66b894b9a5710f1072c8eb6448a7edfc2d743d9", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "6989976265be3f8d2b5802c722f9ba168e227c71", - "version" : "1.7.2" + "revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab", + "version" : "1.6.1" } }, { @@ -96,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", - "version" : "1.5.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -141,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "9c84335373bae5f5c9f7b5f0adf3ae10f2cab5b9", - "version" : "0.25.2" + "revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8", + "version" : "0.31.1" } }, { @@ -154,15 +136,6 @@ "version" : "602.0.0" } }, - { - "identity" : "swift-tagged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-tagged", - "state" : { - "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", - "version" : "0.10.0" - } - }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4962230c22..800ff8ac52 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", - "version" : "1.5.1" + "revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab", + "version" : "1.6.1" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", - "version" : "1.5.0" + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", - "version" : "0.30.0" + "revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8", + "version" : "0.31.1" } }, { diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 81af41ab08..627e1575eb 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -9,7 +9,7 @@ import shared_preferences_foundation import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -21,48 +21,26 @@ import UIKit SwiftNativeVideoPlayerPlugin.cookieStorage = URLSessionManager.cookieStorage URLSessionManager.patchBackgroundDownloader() - GeneratedPluginRegistrant.register(with: self) - let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - AppDelegate.registerPlugins(with: controller.engine, controller: controller) - BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) - - BackgroundServicePlugin.registerBackgroundProcessing() BackgroundWorkerApiImpl.registerBackgroundWorkers() - BackgroundServicePlugin.setPluginRegistrantCallback { registry in - if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.photo-manager") { - PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!) - } - - if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!) - } - - if !registry.hasPlugin("org.cocoapods.permission-handler-apple") { - PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!) - } - - if !registry.hasPlugin("org.cocoapods.network-info-plus") { - FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!) - } - } - return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - - public static func registerPlugins(with engine: FlutterEngine, controller: FlutterViewController?) { - NativeSyncApiImpl.register(with: engine.registrar(forPlugin: NativeSyncApiImpl.name)!) - LocalImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: LocalImageApiImpl()) - RemoteImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: RemoteImageApiImpl()) - BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: BackgroundWorkerApiImpl()) - ConnectivityApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ConnectivityApiImpl()) - NetworkApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: NetworkApiImpl(viewController: controller)) + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + let messenger = engineBridge.applicationRegistrar.messenger() + AppDelegate.registerPlugins(with: engineBridge.pluginRegistry, messenger: messenger) } - + + public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) { + NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!) + LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl()) + RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl()) + BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl()) + ConnectivityApiSetup.setUp(binaryMessenger: messenger, api: ConnectivityApiImpl()) + NetworkApiSetup.setUp(binaryMessenger: messenger, api: NetworkApiImpl()) + } + public static func cancelPlugins(with engine: FlutterEngine) { (engine.valuePublished(byPlugin: NativeSyncApiImpl.name) as? NativeSyncApiImpl)?.detachFromEngine() } diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index 85e1a55d3d..c5b5e1778a 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -95,7 +95,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { // Register plugins in the new engine GeneratedPluginRegistrant.register(with: engine) // Register custom plugins - AppDelegate.registerPlugins(with: engine, controller: nil) + AppDelegate.registerPlugins(with: engine, messenger: engine.binaryMessenger) flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger) BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self) diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift deleted file mode 100644 index cac9faab01..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ /dev/null @@ -1,408 +0,0 @@ -// -// BackgroundServicePlugin.swift -// Runner -// -// Created by Marty Fuhry on 2/14/23. -// - -import Flutter -import BackgroundTasks -import path_provider_foundation -import CryptoKit -import Network - -class BackgroundServicePlugin: NSObject, FlutterPlugin { - - public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - - public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) { - flutterPluginRegistrantCallback = callback - } - - // Pause the application in XCode, then enter - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"] - // or - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"] - // Then resume the application see the background code run - // Tested on a physical device, not a simulator - // This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing. - // In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command - - // This is the task ID in Info.plist to register as our background task ID - public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch" - public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing" - - // Establish communication with the main isolate and set up the channel call - // to this BackgroundServicePlugion() - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "immich/foregroundChannel", - binaryMessenger: registrar.messenger() - ) - - let instance = BackgroundServicePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - registrar.addApplicationDelegate(instance) - } - - // Registers the Flutter engine with the plugins, used by the other Background Flutter engine - public static func register(engine: FlutterEngine) { - GeneratedPluginRegistrant.register(with: engine) - } - - // Registers the task IDs from the system so that we can process them here in this class - public static func registerBackgroundProcessing() { - - let processingRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundProcessingTaskID, - using: nil) { task in - if task is BGProcessingTask { - handleBackgroundProcessing(task: task as! BGProcessingTask) - } - } - - let fetchRegisterd = BGTaskScheduler.shared.register( - forTaskWithIdentifier: backgroundFetchTaskID, - using: nil) { task in - if task is BGAppRefreshTask { - handleBackgroundFetch(task: task as! BGAppRefreshTask) - } - } - } - - // Handles the channel methods from Flutter - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "enable": - handleBackgroundEnable(call: call, result: result) - break - case "configure": - handleConfigure(call: call, result: result) - break - case "disable": - handleDisable(call: call, result: result) - break - case "isEnabled": - handleIsEnabled(call: call, result: result) - break - case "isIgnoringBatteryOptimizations": - result(FlutterMethodNotImplemented) - break - case "lastBackgroundFetchTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time") - result(lastRunTime) - break - case "lastBackgroundProcessingTime": - let defaults = UserDefaults.standard - let lastRunTime = defaults.value(forKey: "last_background_processing_run_time") - result(lastRunTime) - break - case "numberOfBackgroundProcesses": - handleNumberOfProcesses(call: call, result: result) - break - case "backgroundAppRefreshEnabled": - handleBackgroundRefreshStatus(call: call, result: result) - break - case "digestFiles": - handleDigestFiles(call: call, result: result) - break - default: - result(FlutterMethodNotImplemented) - break - } - } - - // Calculates the SHA-1 hash of each file from the list of paths provided - func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) { - - let bufsize = 2 * 1024 * 1024 - // Private error to throw if file cannot be read - enum DigestError: String, LocalizedError { - case NoFileHandle = "Cannot Open File Handle" - - public var errorDescription: String? { self.rawValue } - } - - // Parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(String(describing: call.arguments))") - result(FlutterError(code: "Malformed", - message: "Received args is not an Array", - details: nil)) - return - } - - // Compute hash in background thread - DispatchQueue.global(qos: .background).async { - var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count) - for i in (0 ..< args.count) { - do { - guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle } - var hasher = Insecure.SHA1.init(); - while autoreleasepool(invoking: { - let chunk = file.readData(ofLength: bufsize) - guard !chunk.isEmpty else { return false } // EOF - hasher.update(data: chunk) - return true // continue - }) { } - let digest = hasher.finalize() - hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator()))) - } catch { - print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)") - } - } - - // Return result in main thread - DispatchQueue.main.async { - result(Array(hashes)) - } - } - } - - // Called by the flutter code when enabled so that we can turn on the background services - // and save the callback information to communicate on this method channel - public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to parse the arguments from the method call - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterMethodNotImplemented) - return - } - - // Requires 3 arguments in the array - guard args.count == 3 else { - print("Requires 3 arguments and received \(args.count)") - result(FlutterMethodNotImplemented) - return - } - - // Parses the arguments - let callbackHandle = args[0] as? Int64 - let notificationTitle = args[1] as? String - let instant = args[2] as? Bool - - // Write enabled to settings - let defaults = UserDefaults.standard - - // We are now enabled, so store this - defaults.set(true, forKey: "background_service_enabled") - - // The callback handle is an int64 address to communicate with the main isolate's - // entry function - defaults.set(callbackHandle, forKey: "callback_handle") - - // This is not used yet and will need to be implemented - defaults.set(notificationTitle, forKey: "notification_title") - - // Schedule the background services - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - - result(true) - } - - // Called by the flutter code at launch to see if the background service is enabled or not - func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - let enabled = defaults.value(forKey: "background_service_enabled") as? Bool - - // False by default - result(enabled ?? false) - } - - // Called by the Flutter code whenever a change in configuration is set - func handleConfigure(call: FlutterMethodCall, result: FlutterResult) { - - // Needs to be able to parse the arguments or else fail - guard let args = call.arguments as? Array else { - print("Cannot parse args as array: \(call.arguments)") - result(FlutterError()) - return - } - - // Needs to have 4 arguments in the call or else fail - guard args.count == 4 else { - print("Not enough arguments, 4 required: \(args.count) given") - result(FlutterError()) - return - } - - // Parse the arguments from the method call - let requireUnmeteredNetwork = args[0] as? Bool - let requireCharging = args[1] as? Bool - let triggerUpdateDelay = args[2] as? Int - let triggerMaxDelay = args[3] as? Int - - // Store the values from the call in the defaults - let defaults = UserDefaults.standard - defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network") - defaults.set(requireCharging, forKey: "require_charging") - defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay") - defaults.set(triggerMaxDelay, forKey: "trigger_max_delay") - - // Cancel the background services and reschedule them - BGTaskScheduler.shared.cancelAllTaskRequests() - BackgroundServicePlugin.scheduleBackgroundSync() - BackgroundServicePlugin.scheduleBackgroundFetch() - result(true) - } - - // Returns the number of currently scheduled background processes to Flutter, strictly - // for debugging - func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) { - BGTaskScheduler.shared.getPendingTaskRequests { requests in - result(requests.count) - } - } - - // Disables the service, cancels all the task requests - func handleDisable(call: FlutterMethodCall, result: FlutterResult) { - let defaults = UserDefaults.standard - defaults.set(false, forKey: "background_service_enabled") - - BGTaskScheduler.shared.cancelAllTaskRequests() - result(true) - } - - // Checks the status of the Background App Refresh from the system - // Returns true if the service is enabled for Immich, and false otherwise - func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) { - switch UIApplication.shared.backgroundRefreshStatus { - case .available: - result(true) - break - case .denied: - result(false) - break - case .restricted: - result(false) - break - default: - result(false) - break - } - } - - - // Schedules a short-running background sync to sync only a few photos - static func scheduleBackgroundFetch() { - // We will schedule this task to run no matter the charging or wifi requirents from the end user - // 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings - // 2. We will check the battery connectivity when we begin running the background activity - let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID) - - // Use 5 minutes from now as earliest begin date - backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) - - do { - try BGTaskScheduler.shared.submit(backgroundFetch) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // Schedules a long-running background sync for syncing all of the photos - static func scheduleBackgroundSync() { - let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID) - - // We need the values for requiring charging - let defaults = UserDefaults.standard - let requireCharging = defaults.value(forKey: "require_charging") as? Bool - - // Always require network connectivity, and set the require charging from the above - backgroundProcessing.requiresNetworkConnectivity = true - backgroundProcessing.requiresExternalPower = requireCharging ?? true - - // Use 15 minutes from now as earliest begin date - backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - - do { - // Submit the task to the scheduler - try BGTaskScheduler.shared.submit(backgroundProcessing) - } catch { - print("Could not schedule the background task \(error.localizedDescription)") - } - } - - // This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler - static func handleBackgroundFetch(task: BGAppRefreshTask) { - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time") - - // If we have required charging, we should check the charging status - let requireCharging = defaults.value(forKey: "require_charging") as? Bool ?? false - if (requireCharging) { - UIDevice.current.isBatteryMonitoringEnabled = true - if (UIDevice.current.batteryState == .unplugged) { - // The device is unplugged and we have required charging - // Therefore, we will simply complete the task without - // running it. - task.setTaskCompleted(success: true) - return - } - } - - // If we have required Wi-Fi, we can check the isExpensive property - let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false - if (requireWifi) { - let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi) - let isExpensive = wifiMonitor.currentPath.isExpensive - if (isExpensive) { - // The network is expensive and we have required Wi-Fi - // Therefore, we will simply complete the task without - // running it - task.setTaskCompleted(success: true) - return - } - } - - // Schedule the next sync task so we can run this again later - scheduleBackgroundFetch() - - // The background sync task should only run for 20 seconds at most - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20) - } - - // This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler - static func handleBackgroundProcessing(task: BGProcessingTask) { - // Schedule the next sync task so we run this again later - scheduleBackgroundSync() - - // Log the time of last background processing to now - let defaults = UserDefaults.standard - defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time") - - // We won't specify a max time for the background sync service, so this can run for longer - BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil) - } - - // This is a synchronous function which uses a semaphore to run the background sync worker's run - // function, which will create a background Isolate and communicate with the Flutter code to back - // up the assets. When it completes, we signal the semaphore and complete the execution allowing the - // control to pass back to the caller synchronously - static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) { - - let semaphore = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - let backgroundWorker = BackgroundSyncWorker { _ in - semaphore.signal() - } - task.expirationHandler = { - backgroundWorker.cancel() - task.setTaskCompleted(success: true) - } - - backgroundWorker.run(maxSeconds: maxSeconds) - task.setTaskCompleted(success: true) - } - semaphore.wait() - } - - -} diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift deleted file mode 100644 index 88d9368308..0000000000 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift +++ /dev/null @@ -1,271 +0,0 @@ -// -// BackgroundSyncProcessing.swift -// Runner -// -// Created by Marty Fuhry on 2/6/23. -// -// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift - -import Foundation -import Flutter -import BackgroundTasks - -// The background worker which creates a new Flutter VM, communicates with it -// to run the backup job, and then finishes execution and calls back to its callback -// handler -class BackgroundSyncWorker { - - // The Flutter engine we create for background execution. - // This is not the main Flutter engine which shows the UI, - // this is a brand new isolate created and managed in this code - // here. It does not share memory with the main - // Flutter engine which shows the UI. - // It needs to be started up, registered, and torn down here - let engine: FlutterEngine? = FlutterEngine( - name: "BackgroundImmich" - ) - - let notificationId = "com.alextran.immich/backgroundNotifications" - // The background message passing channel - var channel: FlutterMethodChannel? - - var completionHandler: (UIBackgroundFetchResult) -> Void - let taskSessionStart = Date() - - // We need the completion handler to tell the system when we are done running - init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - - // This is the background message passing channel to be used with the background engine - // created here in this platform code - self.channel = FlutterMethodChannel( - name: "immich/backgroundChannel", - binaryMessenger: engine!.binaryMessenger - ) - self.completionHandler = completionHandler - } - - // Handles all of the messages from the Flutter VM called into this platform code - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialized": - // Initialize tells us that we can now call into the Flutter VM to tell it to begin the update - self.channel?.invokeMethod( - "backgroundProcessing", - arguments: nil, - result: { flutterResult in - - // This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or - // if this execution failed - let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed - - // Show the task duration - let taskSessionCompleter = Date() - let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart) - print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)") - - // Complete the execution - self.complete(result) - }) - break - case "updateNotification": - let handled = self.handleNotification(call) - result(handled) - break - case "showError": - let handled = self.handleError(call) - result(handled) - break - case "clearErrorNotifications": - self.handleClearErrorNotifications() - result(true) - break - case "hasContentChanged": - // This is only called for Android, but we provide an implementation here - // telling Flutter that we don't have any information about whether the gallery - // contents have changed or not, so we can just say "no, they've not changed" - result(false) - break - default: - result(FlutterError()) - self.complete(UIBackgroundFetchResult.failed) - } - } - - // Runs the background sync by starting up a new isolate and handling the calls - // until it completes - public func run(maxSeconds: Int?) { - // We need the callback handle to start up the Flutter VM from the entry point - let defaults = UserDefaults.standard - guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else { - // Can't find the callback handle, this is fatal - complete(UIBackgroundFetchResult.failed) - return - - } - - // Use the provided callbackHandle to get the callback function - guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { - // We need this callback or else this is fatal - complete(UIBackgroundFetchResult.failed) - return - } - - // Sanity check for the engine existing - if engine == nil { - complete(UIBackgroundFetchResult.failed) - return - } - - // Run the engine - let isRunning = engine!.run( - withEntrypoint: callback.callbackName, - libraryURI: callback.callbackLibraryPath - ) - - // If this engine isn't running, this is fatal - if !isRunning { - complete(UIBackgroundFetchResult.failed) - return - } - - // If we have a timer, we need to start the timer to cancel ourselves - // so that we don't run longer than the provided maxSeconds - // After maxSeconds has elapsed, we will invoke "systemStop" - if maxSeconds != nil { - // Schedule a non-repeating timer to run after maxSeconds - let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), - repeats: false) { timer in - // The callback invalidates the timer and stops execution - timer.invalidate() - - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - } - - // Set the handle function to the channel message handler - self.channel?.setMethodCallHandler(handle) - - // Register this to get access to the plugins on the platform channel - BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!) - } - - // Cancels execution of this task, used by the system's task expiration handler - // which is called shortly before execution is about to expire - public func cancel() { - // If the channel is already deallocated, we don't need to do anything - if self.channel == nil { - return - } - - // Tell the Flutter VM to stop backing up now - self.channel?.invokeMethod( - "systemStop", - arguments: nil, - result: nil) - - // Complete the execution - self.complete(UIBackgroundFetchResult.newData) - } - - // Completes the execution, destroys the engine, and sends a completion to our callback completionHandler - private func complete(_ fetchResult: UIBackgroundFetchResult) { - engine?.destroyContext() - channel = nil - completionHandler(fetchResult) - } - - private func handleNotification(_ call: FlutterMethodCall) -> Bool { - - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - print("Failed to parse \(call.arguments) as array") - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 7 else { - print("Needs 7 arguments, but was only passed \(args.count)") - return false - } - - // Parse the arguments to send the notification update - let title = args[0] as? String - let content = args[1] as? String - let progress = args[2] as? Int - let maximum = args[3] as? Int - let indeterminate = args[4] as? Bool - let isDetail = args[5] as? Bool - let onlyIfForeground = args[6] as? Bool - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Uploading..." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) { (error: Error?) in - if let theError = error { - print("Error showing notifications: \(theError)") - } - } - - return true - } - - private func handleError(_ call: FlutterMethodCall) -> Bool { - // Parse the arguments as an array list - guard let args = call.arguments as? Array else { - return false; - } - - // Requires 7 arguments passed or else fail - guard args.count == 3 else { - return false - } - - let title = args[0] as? String - let content = args[1] as? String - let individualTag = args[2] as? String - - // Build the notification - let notificationContent = UNMutableNotificationContent() - notificationContent.body = content ?? "Error running the backup job." - notificationContent.title = title ?? "Immich" - - // Add it to the Notification center - let notification = UNNotificationRequest( - identifier: notificationId, - content: notificationContent, - trigger: nil - ) - let center = UNUserNotificationCenter.current() - center.add(notification) - - return true - } - - private func handleClearErrorNotifications() { - let center = UNUserNotificationCenter.current() - center.removeDeliveredNotifications(withIdentifiers: [notificationId]) - center.removePendingNotificationRequests(withIdentifiers: [notificationId]) - } -} - diff --git a/mobile/ios/Runner/Core/NetworkApiImpl.swift b/mobile/ios/Runner/Core/NetworkApiImpl.swift index 3c4be8e718..82a913d837 100644 --- a/mobile/ios/Runner/Core/NetworkApiImpl.swift +++ b/mobile/ios/Runner/Core/NetworkApiImpl.swift @@ -10,11 +10,14 @@ enum ImportError: Error { } class NetworkApiImpl: NetworkApi { - weak var viewController: UIViewController? private var activeImporter: CertImporter? - - init(viewController: UIViewController?) { - self.viewController = viewController + + private var viewController: UIViewController? { + UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first { $0.isKeyWindow }? + .rootViewController } func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) { diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 3f3735bc16..a37b46b5bc 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -8,8 +8,6 @@ app.alextran.immich.background.refreshUpload app.alextran.immich.background.processingUpload - app.alextran.immich.backgroundFetch - app.alextran.immich.backgroundProcessing CADisableMinimumFrameDurationOnPhone @@ -80,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.7.4 + 2.7.5 CFBundleSignature ???? CFBundleURLTypes @@ -108,8 +106,6 @@ CFBundleVersion 240 - FLTEnableImpeller - ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -180,6 +176,27 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + UIViewControllerBasedStatusBarAppearance io.flutter.embedded_views_preview diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart new file mode 100644 index 0000000000..9159db4ef1 --- /dev/null +++ b/mobile/lib/constants/aspect_ratios.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +enum AspectRatioPreset { + free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded), + square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded), + ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded), + ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded), + ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded), + ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true), + ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true), + ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true); + + final double? ratio; + final String label; + final IconData icon; + final bool iconRotated; + + const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false}); +} diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 9d28941b8f..1748a2a57d 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1,9 +1,5 @@ import 'dart:io'; -const int noDbId = -9223372036854775808; // from Isar -const double downloadCompleted = -1; -const double downloadFailed = -2; - const String kMobileMetadataKey = "mobile-app"; // Number of log entries to retain on app start @@ -47,9 +43,6 @@ const List<(String, String)> kWidgetNames = [ ('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'), ]; -const double kUploadStatusFailed = -1.0; -const double kUploadStatusCanceled = -2.0; - const int kMinMonthsToEnableScrubberSnap = 12; const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652"; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 32ef9bbbed..877145c322 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -11,8 +11,6 @@ enum TextSearchType { context, filename, description, ocr } enum AssetVisibilityEnum { timeline, hidden, archive, locked } -enum SortUserBy { id } - enum ActionSource { timeline, viewer } enum CleanupStep { selectDate, scan, delete } diff --git a/mobile/lib/domain/interfaces/db.interface.dart b/mobile/lib/domain/interfaces/db.interface.dart deleted file mode 100644 index 5645d15c47..0000000000 --- a/mobile/lib/domain/interfaces/db.interface.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class IDatabaseRepository { - Future transaction(Future Function() callback); -} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index cb40c8f76a..85c42fd24f 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -1,3 +1,5 @@ +import 'package:immich_mobile/domain/models/exif.model.dart'; + part 'local_asset.model.dart'; part 'remote_asset.model.dart'; @@ -69,6 +71,8 @@ sealed class BaseAsset { bool get isLocalOnly => storage == AssetState.local; bool get isRemoteOnly => storage == AssetState.remote; + bool get isEditable => false; + // Overridden in subclasses AssetState get storage; String? get localId; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 43d49506e3..745e8f46ff 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset { @override String get heroTag => '${localId ?? checksum}_$id'; + @override + bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage; + @override String toString() { return '''Asset { @@ -128,3 +131,81 @@ class RemoteAsset extends BaseAsset { ); } } + +class RemoteAssetExif extends RemoteAsset { + final ExifInfo exifInfo; + + const RemoteAssetExif({ + required super.id, + super.localId, + required super.name, + required super.ownerId, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + super.thumbHash, + super.visibility = AssetVisibility.timeline, + super.livePhotoVideoId, + super.stackId, + super.isEdited = false, + this.exifInfo = const ExifInfo(), + }); + + @override + bool operator ==(Object other) { + if (other is! RemoteAssetExif) return false; + if (identical(this, other)) return true; + return super == other && exifInfo == other.exifInfo; + } + + @override + int get hashCode => super.hashCode ^ exifInfo.hashCode; + + @override + RemoteAssetExif copyWith({ + String? id, + String? localId, + String? name, + String? ownerId, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + bool? isFavorite, + String? thumbHash, + AssetVisibility? visibility, + String? livePhotoVideoId, + String? stackId, + bool? isEdited, + ExifInfo? exifInfo, + }) { + return RemoteAssetExif( + id: id ?? this.id, + localId: localId ?? this.localId, + name: name ?? this.name, + ownerId: ownerId ?? this.ownerId, + checksum: checksum ?? this.checksum, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + isFavorite: isFavorite ?? this.isFavorite, + thumbHash: thumbHash ?? this.thumbHash, + visibility: visibility ?? this.visibility, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + stackId: stackId ?? this.stackId, + isEdited: isEdited ?? this.isEdited, + exifInfo: exifInfo ?? this.exifInfo, // Use the new parameter + ); + } +} diff --git a/mobile/lib/domain/models/asset_edit.model.dart b/mobile/lib/domain/models/asset_edit.model.dart index b3266dba46..9809b9c606 100644 --- a/mobile/lib/domain/models/asset_edit.model.dart +++ b/mobile/lib/domain/models/asset_edit.model.dart @@ -1,21 +1,25 @@ -import "package:openapi/api.dart" as api show AssetEditAction; +import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters; enum AssetEditAction { rotate, crop, mirror, other } -extension AssetEditActionExtension on AssetEditAction { - api.AssetEditAction? toDto() { - return switch (this) { - AssetEditAction.rotate => api.AssetEditAction.rotate, - AssetEditAction.crop => api.AssetEditAction.crop, - AssetEditAction.mirror => api.AssetEditAction.mirror, - AssetEditAction.other => null, - }; - } +sealed class AssetEdit { + const AssetEdit(); } -class AssetEdit { - final AssetEditAction action; - final Map parameters; +class CropEdit extends AssetEdit { + final CropParameters parameters; - const AssetEdit({required this.action, required this.parameters}); + const CropEdit(this.parameters); +} + +class RotateEdit extends AssetEdit { + final RotateParameters parameters; + + const RotateEdit(this.parameters); +} + +class MirrorEdit extends AssetEdit { + final MirrorParameters parameters; + + const MirrorEdit(this.parameters); } diff --git a/mobile/lib/domain/models/device_asset.model.dart b/mobile/lib/domain/models/device_asset.model.dart deleted file mode 100644 index a404f5a9e2..0000000000 --- a/mobile/lib/domain/models/device_asset.model.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:typed_data'; - -class DeviceAsset { - final String assetId; - final Uint8List hash; - final DateTime modifiedTime; - - const DeviceAsset({required this.assetId, required this.hash, required this.modifiedTime}); - - @override - bool operator ==(covariant DeviceAsset other) { - if (identical(this, other)) return true; - - return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime; - } - - @override - int get hashCode { - return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode; - } - - @override - String toString() { - return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)'; - } - - DeviceAsset copyWith({String? assetId, Uint8List? hash, DateTime? modifiedTime}) { - return DeviceAsset( - assetId: assetId ?? this.assetId, - hash: hash ?? this.hash, - modifiedTime: modifiedTime ?? this.modifiedTime, - ); - } -} diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart index d0f78b59de..45b787d586 100644 --- a/mobile/lib/domain/models/exif.model.dart +++ b/mobile/lib/domain/models/exif.model.dart @@ -7,6 +7,8 @@ class ExifInfo { final String? timeZone; final DateTime? dateTimeOriginal; final int? rating; + final int? width; + final int? height; // GPS final double? latitude; @@ -48,6 +50,8 @@ class ExifInfo { this.timeZone, this.dateTimeOriginal, this.rating, + this.width, + this.height, this.isFlipped = false, this.latitude, this.longitude, @@ -74,6 +78,8 @@ class ExifInfo { other.timeZone == timeZone && other.dateTimeOriginal == dateTimeOriginal && other.rating == rating && + other.width == width && + other.height == height && other.latitude == latitude && other.longitude == longitude && other.city == city && @@ -98,6 +104,8 @@ class ExifInfo { timeZone.hashCode ^ dateTimeOriginal.hashCode ^ rating.hashCode ^ + width.hashCode ^ + height.hashCode ^ latitude.hashCode ^ longitude.hashCode ^ city.hashCode ^ @@ -123,6 +131,8 @@ isFlipped: $isFlipped, timeZone: ${timeZone ?? 'NA'}, dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, rating: ${rating ?? 'NA'}, +width: ${width ?? 'NA'}, +height: ${height ?? 'NA'}, latitude: ${latitude ?? 'NA'}, longitude: ${longitude ?? 'NA'}, city: ${city ?? 'NA'}, @@ -146,6 +156,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, String? timeZone, DateTime? dateTimeOriginal, int? rating, + int? width, + int? height, double? latitude, double? longitude, String? city, @@ -168,6 +180,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, timeZone: timeZone ?? this.timeZone, dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, rating: rating ?? this.rating, + width: width ?? this.width, + height: height ?? this.height, isFlipped: isFlipped ?? this.isFlipped, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 198733b3c8..7fa8c13fd8 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,12 +1,9 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped}); - class AssetService { final RemoteAssetRepository _remoteAssetRepository; final DriftLocalAssetRepository _localAssetRepository; @@ -58,49 +55,6 @@ class AssetService { return _remoteAssetRepository.getExif(id); } - Future getAspectRatio(BaseAsset asset) async { - final dimension = asset is LocalAsset - ? await _getLocalAssetDimensions(asset) - : await _getRemoteAssetDimensions(asset as RemoteAsset); - - if (dimension.width == null || dimension.height == null || dimension.height == 0) { - return 1.0; - } - - return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!; - } - - Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - int orientation = asset.orientation; - - if (width == null || height == null) { - final fetched = await _localAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - orientation = fetched?.orientation ?? 0; - } - - // On Android, local assets need orientation correction for 90°/270° rotations - // On iOS, the Photos framework pre-corrects dimensions - final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270); - return (width: width, height: height, isFlipped: isFlipped); - } - - Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async { - double? width = asset.width?.toDouble(); - double? height = asset.height?.toDouble(); - - if (width == null || height == null) { - final fetched = await _remoteAssetRepository.get(asset.id); - width = fetched?.width?.toDouble(); - height = fetched?.height?.toDouble(); - } - - return (width: width, height: height, isFlipped: false); - } - Future> getPlaces(String userId) { return _remoteAssetRepository.getPlaces(userId); } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 93a2a14127..d4da3e31a4 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -16,19 +16,16 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/wm_executor.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; class BackgroundWorkerFgService { @@ -58,7 +55,6 @@ class BackgroundWorkerFgService { class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ProviderContainer? _ref; - final Isar _isar; final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; @@ -67,18 +63,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { bool _isCleanedUp = false; - BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger}) - : _isar = isar, - _drift = drift, + BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger}) + : _drift = drift, _driftLogger = driftLogger, _backgroundHostApi = BackgroundWorkerBgHostApi() { - _ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); + _ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]); BackgroundWorkerFlutterApi.setUp(this); } @@ -102,7 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ), FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false), FileDownloader().trackTasks(), - _ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(), ].nonNulls, ); @@ -209,9 +197,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { backgroundSyncManager?.cancel(), ]; - if (_isar.isOpen) { - cleanupFutures.add(_isar.close()); - } await Future.wait(cleanupFutures.nonNulls); _logger.info("Background worker resources cleaned up"); } catch (error, stack) { @@ -301,7 +286,6 @@ Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDB) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false); - await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init(); + final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init(); } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 64010b9220..b58ee89535 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -15,7 +15,7 @@ import 'package:logging/logging.dart'; /// via [IStoreRepository] class LogService { final LogRepository _logRepository; - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; final List _msgBuffer = []; @@ -38,7 +38,7 @@ class LogService { static Future init({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { _instance ??= await create( @@ -51,7 +51,7 @@ class LogService { static Future create({ required LogRepository logRepository, - required IStoreRepository storeRepository, + required DriftStoreRepository storeRepository, bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart index 004ad06b1b..8b93e9c8cc 100644 --- a/mobile/lib/domain/services/search.service.dart +++ b/mobile/lib/domain/services/search.service.dart @@ -1,10 +1,9 @@ -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/search_result.model.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility; import 'package:openapi/api.dart' hide AssetVisibility; class SearchService { @@ -52,43 +51,3 @@ class SearchService { return null; } } - -extension on AssetResponseDto { - RemoteAsset toDto() { - return RemoteAsset( - id: id, - name: originalFileName, - checksum: checksum, - createdAt: fileCreatedAt, - updatedAt: updatedAt, - ownerId: ownerId, - visibility: switch (visibility) { - api.AssetVisibility.timeline => AssetVisibility.timeline, - api.AssetVisibility.hidden => AssetVisibility.hidden, - api.AssetVisibility.archive => AssetVisibility.archive, - api.AssetVisibility.locked => AssetVisibility.locked, - _ => AssetVisibility.timeline, - }, - durationInSeconds: duration.toDuration()?.inSeconds ?? 0, - height: height?.toInt(), - width: width?.toInt(), - isFavorite: isFavorite, - livePhotoVideoId: livePhotoVideoId, - thumbHash: thumbhash, - localId: null, - type: type.toAssetType(), - stackId: stack?.id, - isEdited: isEdited, - ); - } -} - -extension on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception('Unknown AssetType value: $this'), - }; -} diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 0098c3d262..b325ffd631 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' /// Provides access to a persistent key-value store with an in-memory cache. /// Listens for repository changes to keep the cache updated. class StoreService { - final IStoreRepository _storeRepository; + final DriftStoreRepository _storeRepository; /// In-memory cache. Keys are [StoreKey.id] final Map _cache = {}; StreamSubscription>? _storeUpdateSubscription; - StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; + StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -24,12 +24,12 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); return _instance!; } - static Future create({required IStoreRepository storeRepository, bool listenUpdates = true}) async { + static Future create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { final instance = StoreService._(isarStoreRepository: storeRepository); await instance.populateCache(); if (listenUpdates) { @@ -91,8 +91,6 @@ class StoreService { await _storeRepository.deleteAll(); _cache.clear(); } - - bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true; } class StoreKeyNotFoundException implements Exception { diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index b33940eacd..a055f8bcae 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -34,6 +34,7 @@ enum TimelineOrigin { search, deepLink, albumActivities, + folder, } class TimelineFactory { diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart index d347d8aa4f..1f9c015ad7 100644 --- a/mobile/lib/domain/services/user.service.dart +++ b/mobile/lib/domain/services/user.service.dart @@ -4,23 +4,17 @@ import 'dart:typed_data'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:logging/logging.dart'; class UserService { final Logger _log = Logger("UserService"); - final IsarUserRepository _isarUserRepository; final UserApiRepository _userApiRepository; final StoreService _storeService; - UserService({ - required IsarUserRepository isarUserRepository, - required UserApiRepository userApiRepository, - required StoreService storeService, - }) : _isarUserRepository = isarUserRepository, - _userApiRepository = userApiRepository, - _storeService = storeService; + UserService({required UserApiRepository userApiRepository, required StoreService storeService}) + : _userApiRepository = userApiRepository, + _storeService = storeService; UserDto getMyUser() { return _storeService.get(StoreKey.currentUser); @@ -38,7 +32,6 @@ class UserService { final user = await _userApiRepository.getMyUser(); if (user == null) return null; await _storeService.put(StoreKey.currentUser, user); - await _isarUserRepository.update(user); return user; } @@ -47,19 +40,10 @@ class UserService { final path = await _userApiRepository.createProfileImage(name: name, data: image); final updatedUser = getMyUser(); await _storeService.put(StoreKey.currentUser, updatedUser); - await _isarUserRepository.update(updatedUser); return path; } catch (e) { _log.warning("Failed to upload profile image", e); return null; } } - - Future> getAll() async { - return await _isarUserRepository.getAll(); - } - - Future deleteAll() { - return _isarUserRepository.deleteAll(); - } } diff --git a/mobile/lib/domain/utils/migrate_cloud_ids.dart b/mobile/lib/domain/utils/migrate_cloud_ids.dart index 33a8eca94d..32188b4838 100644 --- a/mobile/lib/domain/utils/migrate_cloud_ids.dart +++ b/mobile/lib/domain/utils/migrate_cloud_ids.dart @@ -80,12 +80,14 @@ Future _processCloudIdMappingsInBatches( AssetMetadataBulkUpsertItemDto( assetId: mapping.remoteAssetId, key: kMobileMetadataKey, - value: RemoteAssetMobileAppMetadata( - cloudId: mapping.localAsset.cloudId, - createdAt: mapping.localAsset.createdAt.toIso8601String(), - adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), - latitude: mapping.localAsset.latitude?.toString(), - longitude: mapping.localAsset.longitude?.toString(), + value: Map.from( + RemoteAssetMobileAppMetadata( + cloudId: mapping.localAsset.cloudId, + createdAt: mapping.localAsset.createdAt.toIso8601String(), + adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), + latitude: mapping.localAsset.latitude?.toString(), + longitude: mapping.localAsset.longitude?.toString(), + ).toJson(), ), ), ); diff --git a/mobile/lib/entities/README.md b/mobile/lib/entities/README.md deleted file mode 100644 index c2ad4876e3..0000000000 --- a/mobile/lib/entities/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains entity that is stored in the local storage. \ No newline at end of file diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart deleted file mode 100644 index 2ca0d50dcc..0000000000 --- a/mobile/lib/entities/album.entity.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:isar/isar.dart'; -// ignore: implementation_imports -import 'package:isar/src/common/isar_links_common.dart'; -import 'package:openapi/api.dart'; - -part 'album.entity.g.dart'; - -@Collection(inheritance: false) -class Album { - @protected - Album({ - this.remoteId, - this.localId, - required this.name, - required this.createdAt, - required this.modifiedAt, - this.description, - this.startDate, - this.endDate, - this.lastModifiedAssetTimestamp, - required this.shared, - required this.activityEnabled, - this.sortOrder = SortOrder.desc, - }); - - // fields stored in DB - Id id = Isar.autoIncrement; - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - String name; - String? description; - DateTime createdAt; - DateTime modifiedAt; - DateTime? startDate; - DateTime? endDate; - DateTime? lastModifiedAssetTimestamp; - bool shared; - bool activityEnabled; - @enumerated - SortOrder sortOrder; - final IsarLink owner = IsarLink(); - final IsarLink thumbnail = IsarLink(); - final IsarLinks sharedUsers = IsarLinks(); - final IsarLinks assets = IsarLinks(); - - // transient fields - @ignore - bool isAll = false; - - @ignore - String? remoteThumbnailAssetId; - - @ignore - int remoteAssetCount = 0; - - // getters - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isLocal => localId != null; - - @ignore - int get assetCount => assets.length; - - @ignore - String? get ownerId => owner.value?.id; - - @ignore - String? get ownerName { - // Guard null owner - if (owner.value == null) { - return null; - } - - final name = []; - if (owner.value?.name != null) { - name.add(owner.value!.name); - } - - return name.join(' '); - } - - @ignore - String get eTagKeyAssetCount => "device-album-$localId-asset-count"; - - // the following getter are needed because Isar links do not make data - // accessible in an object freshly created (not loaded from DB) - - @ignore - Iterable get remoteUsers => - sharedUsers.isEmpty ? (sharedUsers as IsarLinksCommon).addedObjects : sharedUsers; - - @ignore - Iterable get remoteAssets => assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets; - - @override - bool operator ==(other) { - if (other is! Album) return false; - return id == other.id && - remoteId == other.remoteId && - localId == other.localId && - name == other.name && - description == other.description && - createdAt.isAtSameMomentAs(other.createdAt) && - modifiedAt.isAtSameMomentAs(other.modifiedAt) && - isAtSameMomentAs(startDate, other.startDate) && - isAtSameMomentAs(endDate, other.endDate) && - isAtSameMomentAs(lastModifiedAssetTimestamp, other.lastModifiedAssetTimestamp) && - shared == other.shared && - activityEnabled == other.activityEnabled && - owner.value == other.owner.value && - thumbnail.value == other.thumbnail.value && - sharedUsers.length == other.sharedUsers.length && - assets.length == other.assets.length; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - name.hashCode ^ - createdAt.hashCode ^ - modifiedAt.hashCode ^ - startDate.hashCode ^ - endDate.hashCode ^ - description.hashCode ^ - lastModifiedAssetTimestamp.hashCode ^ - shared.hashCode ^ - activityEnabled.hashCode ^ - owner.value.hashCode ^ - thumbnail.value.hashCode ^ - sharedUsers.length.hashCode ^ - assets.length.hashCode; - - static Future remote(AlbumResponseDto dto) async { - final Isar db = Isar.getInstance()!; - final Album a = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - description: dto.description, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - ); - a.remoteAssetCount = dto.assetCount; - a.owner.value = await db.users.getById(dto.ownerId); - if (dto.order != null) { - a.sortOrder = dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc; - } - - if (dto.albumThumbnailAssetId != null) { - a.thumbnail.value = await db.assets.where().remoteIdEqualTo(dto.albumThumbnailAssetId).findFirst(); - } - if (dto.albumUsers.isNotEmpty) { - final users = await db.users.getAllById(dto.albumUsers.map((e) => e.user.id).toList(growable: false)); - a.sharedUsers.addAll(users.cast()); - } - if (dto.assets.isNotEmpty) { - final assets = await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id)); - a.assets.addAll(assets); - } - return a; - } - - @override - String toString() => 'remoteId: $remoteId name: $name description: $description'; -} - -extension AssetsHelper on IsarCollection { - Future store(Album a) async { - await put(a); - await a.owner.save(); - await a.thumbnail.save(); - await a.sharedUsers.save(); - await a.assets.save(); - return a; - } -} diff --git a/mobile/lib/entities/album.entity.g.dart b/mobile/lib/entities/album.entity.g.dart deleted file mode 100644 index ecbbab48c2..0000000000 --- a/mobile/lib/entities/album.entity.g.dart +++ /dev/null @@ -1,2240 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAlbumCollection on Isar { - IsarCollection get albums => this.collection(); -} - -const AlbumSchema = CollectionSchema( - name: r'Album', - id: -1355968412107120937, - properties: { - r'activityEnabled': PropertySchema( - id: 0, - name: r'activityEnabled', - type: IsarType.bool, - ), - r'createdAt': PropertySchema( - id: 1, - name: r'createdAt', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 2, - name: r'description', - type: IsarType.string, - ), - r'endDate': PropertySchema( - id: 3, - name: r'endDate', - type: IsarType.dateTime, - ), - r'lastModifiedAssetTimestamp': PropertySchema( - id: 4, - name: r'lastModifiedAssetTimestamp', - type: IsarType.dateTime, - ), - r'localId': PropertySchema(id: 5, name: r'localId', type: IsarType.string), - r'modifiedAt': PropertySchema( - id: 6, - name: r'modifiedAt', - type: IsarType.dateTime, - ), - r'name': PropertySchema(id: 7, name: r'name', type: IsarType.string), - r'remoteId': PropertySchema( - id: 8, - name: r'remoteId', - type: IsarType.string, - ), - r'shared': PropertySchema(id: 9, name: r'shared', type: IsarType.bool), - r'sortOrder': PropertySchema( - id: 10, - name: r'sortOrder', - type: IsarType.byte, - enumMap: _AlbumsortOrderEnumValueMap, - ), - r'startDate': PropertySchema( - id: 11, - name: r'startDate', - type: IsarType.dateTime, - ), - }, - - estimateSize: _albumEstimateSize, - serialize: _albumSerialize, - deserialize: _albumDeserialize, - deserializeProp: _albumDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: { - r'owner': LinkSchema( - id: 8272576585804958029, - name: r'owner', - target: r'User', - single: true, - ), - r'thumbnail': LinkSchema( - id: 4055421409629988258, - name: r'thumbnail', - target: r'Asset', - single: true, - ), - r'sharedUsers': LinkSchema( - id: 8972835302564625434, - name: r'sharedUsers', - target: r'User', - single: false, - ), - r'assets': LinkSchema( - id: 1059358332698388152, - name: r'assets', - target: r'Asset', - single: false, - ), - }, - embeddedSchemas: {}, - - getId: _albumGetId, - getLinks: _albumGetLinks, - attach: _albumAttach, - version: '3.3.0-dev.3', -); - -int _albumEstimateSize( - Album object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.name.length * 3; - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _albumSerialize( - Album object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeBool(offsets[0], object.activityEnabled); - writer.writeDateTime(offsets[1], object.createdAt); - writer.writeString(offsets[2], object.description); - writer.writeDateTime(offsets[3], object.endDate); - writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp); - writer.writeString(offsets[5], object.localId); - writer.writeDateTime(offsets[6], object.modifiedAt); - writer.writeString(offsets[7], object.name); - writer.writeString(offsets[8], object.remoteId); - writer.writeBool(offsets[9], object.shared); - writer.writeByte(offsets[10], object.sortOrder.index); - writer.writeDateTime(offsets[11], object.startDate); -} - -Album _albumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Album( - activityEnabled: reader.readBool(offsets[0]), - createdAt: reader.readDateTime(offsets[1]), - description: reader.readStringOrNull(offsets[2]), - endDate: reader.readDateTimeOrNull(offsets[3]), - lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]), - localId: reader.readStringOrNull(offsets[5]), - modifiedAt: reader.readDateTime(offsets[6]), - name: reader.readString(offsets[7]), - remoteId: reader.readStringOrNull(offsets[8]), - shared: reader.readBool(offsets[9]), - sortOrder: - _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ?? - SortOrder.desc, - startDate: reader.readDateTimeOrNull(offsets[11]), - ); - object.id = id; - return object; -} - -P _albumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readBool(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (reader.readStringOrNull(offset)) as P; - case 3: - return (reader.readDateTimeOrNull(offset)) as P; - case 4: - return (reader.readDateTimeOrNull(offset)) as P; - case 5: - return (reader.readStringOrNull(offset)) as P; - case 6: - return (reader.readDateTime(offset)) as P; - case 7: - return (reader.readString(offset)) as P; - case 8: - return (reader.readStringOrNull(offset)) as P; - case 9: - return (reader.readBool(offset)) as P; - case 10: - return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ?? - SortOrder.desc) - as P; - case 11: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AlbumsortOrderEnumValueMap = {'asc': 0, 'desc': 1}; -const _AlbumsortOrderValueEnumMap = {0: SortOrder.asc, 1: SortOrder.desc}; - -Id _albumGetId(Album object) { - return object.id; -} - -List> _albumGetLinks(Album object) { - return [object.owner, object.thumbnail, object.sharedUsers, object.assets]; -} - -void _albumAttach(IsarCollection col, Id id, Album object) { - object.id = id; - object.owner.attach(col, col.isar.collection(), r'owner', id); - object.thumbnail.attach(col, col.isar.collection(), r'thumbnail', id); - object.sharedUsers.attach( - col, - col.isar.collection(), - r'sharedUsers', - id, - ); - object.assets.attach(col, col.isar.collection(), r'assets', id); -} - -extension AlbumQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AlbumQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AlbumQueryFilter on QueryBuilder { - QueryBuilder activityEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'activityEnabled', value: value), - ); - }); - } - - QueryBuilder createdAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'createdAt', value: value), - ); - }); - } - - QueryBuilder createdAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'createdAt', - value: value, - ), - ); - }); - } - - QueryBuilder createdAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'createdAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder endDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'endDate'), - ); - }); - } - - QueryBuilder endDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'endDate', value: value), - ); - }); - } - - QueryBuilder endDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'endDate', - value: value, - ), - ); - }); - } - - QueryBuilder endDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'endDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lastModifiedAssetTimestamp'), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull( - property: r'lastModifiedAssetTimestamp', - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastModifiedAssetTimestamp', - value: value, - ), - ); - }); - } - - QueryBuilder - lastModifiedAssetTimestampBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastModifiedAssetTimestamp', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder modifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedAt', value: value), - ); - }); - } - - QueryBuilder modifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder modifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder sharedEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'shared', value: value), - ); - }); - } - - QueryBuilder sortOrderEqualTo( - SortOrder value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'sortOrder', value: value), - ); - }); - } - - QueryBuilder sortOrderGreaterThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderLessThan( - SortOrder value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'sortOrder', - value: value, - ), - ); - }); - } - - QueryBuilder sortOrderBetween( - SortOrder lower, - SortOrder upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'sortOrder', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder startDateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'startDate'), - ); - }); - } - - QueryBuilder startDateEqualTo( - DateTime? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'startDate', value: value), - ); - }); - } - - QueryBuilder startDateGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'startDate', - value: value, - ), - ); - }); - } - - QueryBuilder startDateBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'startDate', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AlbumQueryObject on QueryBuilder {} - -extension AlbumQueryLinks on QueryBuilder { - QueryBuilder owner(FilterQuery q) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'owner'); - }); - } - - QueryBuilder ownerIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'owner', 0, true, 0, true); - }); - } - - QueryBuilder thumbnail( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'thumbnail'); - }); - } - - QueryBuilder thumbnailIsNull() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'thumbnail', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsers( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'sharedUsers'); - }); - } - - QueryBuilder sharedUsersLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, true, length, true); - }); - } - - QueryBuilder sharedUsersIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, 0, true); - }); - } - - QueryBuilder sharedUsersIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, false, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', 0, true, length, include); - }); - } - - QueryBuilder - sharedUsersLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'sharedUsers', length, include, 999999, true); - }); - } - - QueryBuilder sharedUsersLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'sharedUsers', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder assets( - FilterQuery q, - ) { - return QueryBuilder.apply(this, (query) { - return query.link(q, r'assets'); - }); - } - - QueryBuilder assetsLengthEqualTo( - int length, - ) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, true, length, true); - }); - } - - QueryBuilder assetsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, 0, true); - }); - } - - QueryBuilder assetsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, false, 999999, true); - }); - } - - QueryBuilder assetsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', 0, true, length, include); - }); - } - - QueryBuilder assetsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength(r'assets', length, include, 999999, true); - }); - } - - QueryBuilder assetsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.linkLength( - r'assets', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } -} - -extension AlbumQuerySortBy on QueryBuilder { - QueryBuilder sortByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder sortByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder sortByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder sortByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder sortByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder sortByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - sortByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder sortBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder sortBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder sortBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder sortByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder sortByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQuerySortThenBy on QueryBuilder { - QueryBuilder thenByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.asc); - }); - } - - QueryBuilder thenByActivityEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'activityEnabled', Sort.desc); - }); - } - - QueryBuilder thenByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.asc); - }); - } - - QueryBuilder thenByCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'createdAt', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.asc); - }); - } - - QueryBuilder thenByEndDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'endDate', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.asc); - }); - } - - QueryBuilder - thenByLastModifiedAssetTimestampDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModifiedAssetTimestamp', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByShared() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.asc); - }); - } - - QueryBuilder thenBySharedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'shared', Sort.desc); - }); - } - - QueryBuilder thenBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.asc); - }); - } - - QueryBuilder thenBySortOrderDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'sortOrder', Sort.desc); - }); - } - - QueryBuilder thenByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.asc); - }); - } - - QueryBuilder thenByStartDateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'startDate', Sort.desc); - }); - } -} - -extension AlbumQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByActivityEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'activityEnabled'); - }); - } - - QueryBuilder distinctByCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'createdAt'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByEndDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'endDate'); - }); - } - - QueryBuilder distinctByLastModifiedAssetTimestamp() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedAt'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByShared() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'shared'); - }); - } - - QueryBuilder distinctBySortOrder() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'sortOrder'); - }); - } - - QueryBuilder distinctByStartDate() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'startDate'); - }); - } -} - -extension AlbumQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder activityEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'activityEnabled'); - }); - } - - QueryBuilder createdAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'createdAt'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder endDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'endDate'); - }); - } - - QueryBuilder - lastModifiedAssetTimestampProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastModifiedAssetTimestamp'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder modifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedAt'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder sharedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'shared'); - }); - } - - QueryBuilder sortOrderProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'sortOrder'); - }); - } - - QueryBuilder startDateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'startDate'); - }); - } -} diff --git a/mobile/lib/entities/android_device_asset.entity.dart b/mobile/lib/entities/android_device_asset.entity.dart deleted file mode 100644 index 792de346b9..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:isar/isar.dart'; - -part 'android_device_asset.entity.g.dart'; - -@Collection() -class AndroidDeviceAsset extends DeviceAsset { - AndroidDeviceAsset({required this.id, required super.hash}); - Id id; -} diff --git a/mobile/lib/entities/android_device_asset.entity.g.dart b/mobile/lib/entities/android_device_asset.entity.g.dart deleted file mode 100644 index f8b1e32c72..0000000000 --- a/mobile/lib/entities/android_device_asset.entity.g.dart +++ /dev/null @@ -1,463 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'android_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAndroidDeviceAssetCollection on Isar { - IsarCollection get androidDeviceAssets => - this.collection(); -} - -const AndroidDeviceAssetSchema = CollectionSchema( - name: r'AndroidDeviceAsset', - id: -6758387181232899335, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - }, - - estimateSize: _androidDeviceAssetEstimateSize, - serialize: _androidDeviceAssetSerialize, - deserialize: _androidDeviceAssetDeserialize, - deserializeProp: _androidDeviceAssetDeserializeProp, - idName: r'id', - indexes: { - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _androidDeviceAssetGetId, - getLinks: _androidDeviceAssetGetLinks, - attach: _androidDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _androidDeviceAssetEstimateSize( - AndroidDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _androidDeviceAssetSerialize( - AndroidDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); -} - -AndroidDeviceAsset _androidDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = AndroidDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: id, - ); - return object; -} - -P _androidDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _androidDeviceAssetGetId(AndroidDeviceAsset object) { - return object.id; -} - -List> _androidDeviceAssetGetLinks( - AndroidDeviceAsset object, -) { - return []; -} - -void _androidDeviceAssetAttach( - IsarCollection col, - Id id, - AndroidDeviceAsset object, -) { - object.id = id; -} - -extension AndroidDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AndroidDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AndroidDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AndroidDeviceAssetQueryObject - on QueryBuilder {} - -extension AndroidDeviceAssetQueryLinks - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortBy - on QueryBuilder {} - -extension AndroidDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension AndroidDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } -} - -extension AndroidDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } -} diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart deleted file mode 100644 index 0d549457a1..0000000000 --- a/mobile/lib/entities/asset.entity.dart +++ /dev/null @@ -1,575 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:photo_manager/photo_manager.dart' show AssetEntity; - -part 'asset.entity.g.dart'; - -/// Asset (online or local) -@Collection(inheritance: false) -class Asset { - Asset.remote(AssetResponseDto remote) - : remoteId = remote.id, - checksum = remote.checksum, - fileCreatedAt = remote.fileCreatedAt, - fileModifiedAt = remote.fileModifiedAt, - updatedAt = remote.updatedAt, - durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, - type = remote.type.toAssetType(), - fileName = remote.originalFileName, - height = remote.exifInfo?.exifImageHeight?.toInt(), - width = remote.exifInfo?.exifImageWidth?.toInt(), - livePhotoVideoId = remote.livePhotoVideoId, - ownerId = fastHash(remote.ownerId), - exifInfo = remote.exifInfo == null ? null : ExifDtoConverter.fromDto(remote.exifInfo!), - isFavorite = remote.isFavorite, - isArchived = remote.isArchived, - isTrashed = remote.isTrashed, - isOffline = remote.isOffline, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id ? null : remote.stack?.primaryAssetId, - stackCount = remote.stack?.assetCount ?? 0, - stackId = remote.stack?.id, - thumbhash = remote.thumbhash, - visibility = getVisibility(remote.visibility); - - Asset({ - this.id = Isar.autoIncrement, - required this.checksum, - this.remoteId, - required this.localId, - required this.ownerId, - required this.fileCreatedAt, - required this.fileModifiedAt, - required this.updatedAt, - required this.durationInSeconds, - required this.type, - this.width, - this.height, - required this.fileName, - this.livePhotoVideoId, - this.exifInfo, - this.isFavorite = false, - this.isArchived = false, - this.isTrashed = false, - this.stackId, - this.stackPrimaryAssetId, - this.stackCount = 0, - this.isOffline = false, - this.thumbhash, - this.visibility = AssetVisibilityEnum.timeline, - }); - - @ignore - AssetEntity? _local; - - @ignore - AssetEntity? get local { - if (isLocal && _local == null) { - _local = AssetEntity( - id: localId!, - typeInt: isImage ? 1 : 2, - width: width ?? 0, - height: height ?? 0, - duration: durationInSeconds, - createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000, - modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000, - title: fileName, - ); - } - return _local; - } - - set local(AssetEntity? assetEntity) => _local = assetEntity; - - @ignore - bool _didUpdateLocal = false; - - @ignore - Future get localAsync async { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - - final updatedLocal = _didUpdateLocal ? local : await local.obtainForNewProperties(); - if (updatedLocal == null) { - throw Exception('Could not fetch local data for $fileName'); - } - - this.local = updatedLocal; - _didUpdateLocal = true; - return updatedLocal; - } - - Id id = Isar.autoIncrement; - - /// stores the raw SHA1 bytes as a base64 String - /// because Isar cannot sort lists of byte arrays - String checksum; - - String? thumbhash; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? remoteId; - - @Index(unique: false, replace: false, type: IndexType.hash) - String? localId; - - @Index(unique: true, replace: false, composite: [CompositeIndex("checksum", type: IndexType.hash)]) - int ownerId; - - DateTime fileCreatedAt; - - DateTime fileModifiedAt; - - DateTime updatedAt; - - int durationInSeconds; - - @Enumerated(EnumType.ordinal) - AssetType type; - - short? width; - - short? height; - - String fileName; - - String? livePhotoVideoId; - - bool isFavorite; - - bool isArchived; - - bool isTrashed; - - bool isOffline; - - @ignore - ExifInfo? exifInfo; - - String? stackId; - - String? stackPrimaryAssetId; - - int stackCount; - - @Enumerated(EnumType.ordinal) - AssetVisibilityEnum visibility; - - /// Returns null if the asset has no sync access to the exif info - @ignore - double? get aspectRatio { - final orientatedWidth = this.orientatedWidth; - final orientatedHeight = this.orientatedHeight; - - if (orientatedWidth != null && orientatedHeight != null && orientatedWidth > 0 && orientatedHeight > 0) { - return orientatedWidth.toDouble() / orientatedHeight.toDouble(); - } - - return null; - } - - /// `true` if this [Asset] is present on the device - @ignore - bool get isLocal => localId != null; - - @ignore - bool get isInDb => id != Isar.autoIncrement; - - @ignore - String get name => p.withoutExtension(fileName); - - /// `true` if this [Asset] is present on the server - @ignore - bool get isRemote => remoteId != null; - - @ignore - bool get isImage => type == AssetType.image; - - @ignore - bool get isVideo => type == AssetType.video; - - @ignore - bool get isMotionPhoto => livePhotoVideoId != null; - - @ignore - AssetState get storage { - if (isRemote && isLocal) { - return AssetState.merged; - } else if (isRemote) { - return AssetState.remote; - } else if (isLocal) { - return AssetState.local; - } else { - throw Exception("Asset has illegal state: $this"); - } - } - - @ignore - Duration get duration => Duration(seconds: durationInSeconds); - - // ignore: invalid_annotation_target - @ignore - set byteHash(List hash) => checksum = base64.encode(hash); - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - bool? get isFlipped { - final exifInfo = this.exifInfo; - if (exifInfo != null) { - return exifInfo.isFlipped; - } - - if (_didUpdateLocal && Platform.isAndroid) { - final local = this.local; - if (local == null) { - throw Exception('Asset $fileName has no local data'); - } - return local.orientation == 90 || local.orientation == 270; - } - - return null; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedHeight { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? width : height; - } - - /// Returns null if the asset has no sync access to the exif info - @ignore - @pragma('vm:prefer-inline') - int? get orientatedWidth { - final isFlipped = this.isFlipped; - if (isFlipped == null) { - return null; - } - - return isFlipped ? height : width; - } - - @override - bool operator ==(other) { - if (other is! Asset) return false; - if (identical(this, other)) return true; - return id == other.id && - checksum == other.checksum && - remoteId == other.remoteId && - localId == other.localId && - ownerId == other.ownerId && - fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) && - fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) && - updatedAt.isAtSameMomentAs(other.updatedAt) && - durationInSeconds == other.durationInSeconds && - type == other.type && - width == other.width && - height == other.height && - fileName == other.fileName && - livePhotoVideoId == other.livePhotoVideoId && - isFavorite == other.isFavorite && - isLocal == other.isLocal && - isArchived == other.isArchived && - isTrashed == other.isTrashed && - stackCount == other.stackCount && - stackPrimaryAssetId == other.stackPrimaryAssetId && - stackId == other.stackId; - } - - @override - @ignore - int get hashCode => - id.hashCode ^ - checksum.hashCode ^ - remoteId.hashCode ^ - localId.hashCode ^ - ownerId.hashCode ^ - fileCreatedAt.hashCode ^ - fileModifiedAt.hashCode ^ - updatedAt.hashCode ^ - durationInSeconds.hashCode ^ - type.hashCode ^ - width.hashCode ^ - height.hashCode ^ - fileName.hashCode ^ - livePhotoVideoId.hashCode ^ - isFavorite.hashCode ^ - isLocal.hashCode ^ - isArchived.hashCode ^ - isTrashed.hashCode ^ - stackCount.hashCode ^ - stackPrimaryAssetId.hashCode ^ - stackId.hashCode; - - /// Returns `true` if this [Asset] can updated with values from parameter [a] - bool canUpdate(Asset a) { - assert(isInDb); - assert(checksum == a.checksum); - assert(a.storage != AssetState.merged); - return a.updatedAt.isAfter(updatedAt) || - a.isRemote && !isRemote || - a.isLocal && !isLocal || - width == null && a.width != null || - height == null && a.height != null || - livePhotoVideoId == null && a.livePhotoVideoId != null || - isFavorite != a.isFavorite || - isArchived != a.isArchived || - isTrashed != a.isTrashed || - isOffline != a.isOffline || - a.exifInfo?.latitude != exifInfo?.latitude || - a.exifInfo?.longitude != exifInfo?.longitude || - // no local stack count or different count from remote - a.thumbhash != thumbhash || - stackId != a.stackId || - stackCount != a.stackCount || - stackPrimaryAssetId == null && a.stackPrimaryAssetId != null || - visibility != a.visibility; - } - - /// Returns a new [Asset] with values from this and merged & updated with [a] - Asset updatedCopy(Asset a) { - assert(canUpdate(a)); - if (a.updatedAt.isAfter(updatedAt)) { - // take most values from newer asset - // keep vales that can never be set by the asset not in DB - if (a.isRemote) { - return a.copyWith( - id: id, - localId: localId, - width: a.width ?? width, - height: a.height ?? height, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - ); - } else if (isRemote) { - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), - ); - } else { - // TODO: Revisit this and remove all bool field assignments - return a.copyWith( - id: id, - remoteId: remoteId, - livePhotoVideoId: livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: stackId, - stackPrimaryAssetId: stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, - stackCount: stackCount, - isFavorite: isFavorite, - isArchived: isArchived, - isTrashed: isTrashed, - isOffline: isOffline, - ); - } - } else { - // fill in potentially missing values, i.e. merge assets - if (a.isRemote) { - // values from remote take precedence - return copyWith( - remoteId: a.remoteId, - width: a.width, - height: a.height, - livePhotoVideoId: a.livePhotoVideoId, - // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app - // stack handling to properly handle it - stackId: a.stackId, - stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId ? null : a.stackPrimaryAssetId, - stackCount: a.stackCount, - // isFavorite + isArchived are not set by device-only assets - isFavorite: a.isFavorite, - isArchived: a.isArchived, - isTrashed: a.isTrashed, - isOffline: a.isOffline, - exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo, - thumbhash: a.thumbhash, - ); - } else { - // add only missing values (and set isLocal to true) - return copyWith( - localId: localId ?? a.localId, - width: width ?? a.width, - height: height ?? a.height, - exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), // updated to use assetId - ); - } - } - } - - Asset copyWith({ - Id? id, - String? checksum, - String? remoteId, - String? localId, - int? ownerId, - DateTime? fileCreatedAt, - DateTime? fileModifiedAt, - DateTime? updatedAt, - int? durationInSeconds, - AssetType? type, - short? width, - short? height, - String? fileName, - String? livePhotoVideoId, - bool? isFavorite, - bool? isArchived, - bool? isTrashed, - bool? isOffline, - ExifInfo? exifInfo, - String? stackId, - String? stackPrimaryAssetId, - int? stackCount, - String? thumbhash, - AssetVisibilityEnum? visibility, - }) => Asset( - id: id ?? this.id, - checksum: checksum ?? this.checksum, - remoteId: remoteId ?? this.remoteId, - localId: localId ?? this.localId, - ownerId: ownerId ?? this.ownerId, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt, - updatedAt: updatedAt ?? this.updatedAt, - durationInSeconds: durationInSeconds ?? this.durationInSeconds, - type: type ?? this.type, - width: width ?? this.width, - height: height ?? this.height, - fileName: fileName ?? this.fileName, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - isFavorite: isFavorite ?? this.isFavorite, - isArchived: isArchived ?? this.isArchived, - isTrashed: isTrashed ?? this.isTrashed, - isOffline: isOffline ?? this.isOffline, - exifInfo: exifInfo ?? this.exifInfo, - stackId: stackId ?? this.stackId, - stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, - stackCount: stackCount ?? this.stackCount, - thumbhash: thumbhash ?? this.thumbhash, - visibility: visibility ?? this.visibility, - ); - - Future put(Isar db) async { - await db.assets.put(this); - if (exifInfo != null) { - await db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id))); - } - } - - static int compareById(Asset a, Asset b) => a.id.compareTo(b.id); - - static int compareByLocalId(Asset a, Asset b) => compareToNullable(a.localId, b.localId); - - static int compareByChecksum(Asset a, Asset b) => a.checksum.compareTo(b.checksum); - - static int compareByOwnerChecksum(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - return compareByChecksum(a, b); - } - - static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) { - final int ownerIdOrder = a.ownerId.compareTo(b.ownerId); - if (ownerIdOrder != 0) return ownerIdOrder; - final int checksumOrder = compareByChecksum(a, b); - if (checksumOrder != 0) return checksumOrder; - final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt); - if (createdOrder != 0) return createdOrder; - return a.fileModifiedAt.compareTo(b.fileModifiedAt); - } - - @override - String toString() { - return """ -{ - "id": ${id == Isar.autoIncrement ? '"N/A"' : id}, - "remoteId": "${remoteId ?? "N/A"}", - "localId": "${localId ?? "N/A"}", - "checksum": "$checksum", - "ownerId": $ownerId, - "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", - "stackId": "${stackId ?? "N/A"}", - "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}", - "stackCount": "$stackCount", - "fileCreatedAt": "$fileCreatedAt", - "fileModifiedAt": "$fileModifiedAt", - "updatedAt": "$updatedAt", - "durationInSeconds": $durationInSeconds, - "type": "$type", - "fileName": "$fileName", - "isFavorite": $isFavorite, - "isRemote": $isRemote, - "storage": "$storage", - "width": ${width ?? "N/A"}, - "height": ${height ?? "N/A"}, - "isArchived": $isArchived, - "isTrashed": $isTrashed, - "isOffline": $isOffline, - "visibility": "$visibility", -}"""; - } - - static getVisibility(AssetVisibility visibility) => switch (visibility) { - AssetVisibility.archive => AssetVisibilityEnum.archive, - AssetVisibility.hidden => AssetVisibilityEnum.hidden, - AssetVisibility.locked => AssetVisibilityEnum.locked, - AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline, - }; -} - -enum AssetType { - // do not change this order! - other, - image, - video, - audio, -} - -extension AssetTypeEnumHelper on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception(), - }; -} - -/// Describes where the information of this asset came from: -/// only from the local device, only from the remote server or merged from both -enum AssetState { local, remote, merged } - -extension AssetsHelper on IsarCollection { - Future deleteAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value(0) : remote(ids).deleteAll(); - Future deleteAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value(0) : local(ids).deleteAll(); - Future> getAllByRemoteId(Iterable ids) => ids.isEmpty ? Future.value([]) : remote(ids).findAll(); - Future> getAllByLocalId(Iterable ids) => ids.isEmpty ? Future.value([]) : local(ids).findAll(); - Future getByRemoteId(String id) => where().remoteIdEqualTo(id).findFirst(); - - QueryBuilder remote(Iterable ids) => - where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e)); - QueryBuilder local(Iterable ids) { - return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e)); - } -} diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart deleted file mode 100644 index db6bc72331..0000000000 --- a/mobile/lib/entities/asset.entity.g.dart +++ /dev/null @@ -1,3711 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetAssetCollection on Isar { - IsarCollection get assets => this.collection(); -} - -const AssetSchema = CollectionSchema( - name: r'Asset', - id: -2933289051367723566, - properties: { - r'checksum': PropertySchema( - id: 0, - name: r'checksum', - type: IsarType.string, - ), - r'durationInSeconds': PropertySchema( - id: 1, - name: r'durationInSeconds', - type: IsarType.long, - ), - r'fileCreatedAt': PropertySchema( - id: 2, - name: r'fileCreatedAt', - type: IsarType.dateTime, - ), - r'fileModifiedAt': PropertySchema( - id: 3, - name: r'fileModifiedAt', - type: IsarType.dateTime, - ), - r'fileName': PropertySchema( - id: 4, - name: r'fileName', - type: IsarType.string, - ), - r'height': PropertySchema(id: 5, name: r'height', type: IsarType.int), - r'isArchived': PropertySchema( - id: 6, - name: r'isArchived', - type: IsarType.bool, - ), - r'isFavorite': PropertySchema( - id: 7, - name: r'isFavorite', - type: IsarType.bool, - ), - r'isOffline': PropertySchema( - id: 8, - name: r'isOffline', - type: IsarType.bool, - ), - r'isTrashed': PropertySchema( - id: 9, - name: r'isTrashed', - type: IsarType.bool, - ), - r'livePhotoVideoId': PropertySchema( - id: 10, - name: r'livePhotoVideoId', - type: IsarType.string, - ), - r'localId': PropertySchema(id: 11, name: r'localId', type: IsarType.string), - r'ownerId': PropertySchema(id: 12, name: r'ownerId', type: IsarType.long), - r'remoteId': PropertySchema( - id: 13, - name: r'remoteId', - type: IsarType.string, - ), - r'stackCount': PropertySchema( - id: 14, - name: r'stackCount', - type: IsarType.long, - ), - r'stackId': PropertySchema(id: 15, name: r'stackId', type: IsarType.string), - r'stackPrimaryAssetId': PropertySchema( - id: 16, - name: r'stackPrimaryAssetId', - type: IsarType.string, - ), - r'thumbhash': PropertySchema( - id: 17, - name: r'thumbhash', - type: IsarType.string, - ), - r'type': PropertySchema( - id: 18, - name: r'type', - type: IsarType.byte, - enumMap: _AssettypeEnumValueMap, - ), - r'updatedAt': PropertySchema( - id: 19, - name: r'updatedAt', - type: IsarType.dateTime, - ), - r'visibility': PropertySchema( - id: 20, - name: r'visibility', - type: IsarType.byte, - enumMap: _AssetvisibilityEnumValueMap, - ), - r'width': PropertySchema(id: 21, name: r'width', type: IsarType.int), - }, - - estimateSize: _assetEstimateSize, - serialize: _assetSerialize, - deserialize: _assetDeserialize, - deserializeProp: _assetDeserializeProp, - idName: r'id', - indexes: { - r'remoteId': IndexSchema( - id: 6301175856541681032, - name: r'remoteId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'remoteId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'localId': IndexSchema( - id: 1199848425898359622, - name: r'localId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'localId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'ownerId_checksum': IndexSchema( - id: -3295822444433175883, - name: r'ownerId_checksum', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'ownerId', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'checksum', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _assetGetId, - getLinks: _assetGetLinks, - attach: _assetAttach, - version: '3.3.0-dev.3', -); - -int _assetEstimateSize( - Asset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.checksum.length * 3; - bytesCount += 3 + object.fileName.length * 3; - { - final value = object.livePhotoVideoId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.localId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.remoteId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.stackPrimaryAssetId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.thumbhash; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _assetSerialize( - Asset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.checksum); - writer.writeLong(offsets[1], object.durationInSeconds); - writer.writeDateTime(offsets[2], object.fileCreatedAt); - writer.writeDateTime(offsets[3], object.fileModifiedAt); - writer.writeString(offsets[4], object.fileName); - writer.writeInt(offsets[5], object.height); - writer.writeBool(offsets[6], object.isArchived); - writer.writeBool(offsets[7], object.isFavorite); - writer.writeBool(offsets[8], object.isOffline); - writer.writeBool(offsets[9], object.isTrashed); - writer.writeString(offsets[10], object.livePhotoVideoId); - writer.writeString(offsets[11], object.localId); - writer.writeLong(offsets[12], object.ownerId); - writer.writeString(offsets[13], object.remoteId); - writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackId); - writer.writeString(offsets[16], object.stackPrimaryAssetId); - writer.writeString(offsets[17], object.thumbhash); - writer.writeByte(offsets[18], object.type.index); - writer.writeDateTime(offsets[19], object.updatedAt); - writer.writeByte(offsets[20], object.visibility.index); - writer.writeInt(offsets[21], object.width); -} - -Asset _assetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Asset( - checksum: reader.readString(offsets[0]), - durationInSeconds: reader.readLong(offsets[1]), - fileCreatedAt: reader.readDateTime(offsets[2]), - fileModifiedAt: reader.readDateTime(offsets[3]), - fileName: reader.readString(offsets[4]), - height: reader.readIntOrNull(offsets[5]), - id: id, - isArchived: reader.readBoolOrNull(offsets[6]) ?? false, - isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isOffline: reader.readBoolOrNull(offsets[8]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[10]), - localId: reader.readStringOrNull(offsets[11]), - ownerId: reader.readLong(offsets[12]), - remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]) ?? 0, - stackId: reader.readStringOrNull(offsets[15]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), - thumbhash: reader.readStringOrNull(offsets[17]), - type: - _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? - AssetType.other, - updatedAt: reader.readDateTime(offsets[19]), - visibility: - _AssetvisibilityValueEnumMap[reader.readByteOrNull(offsets[20])] ?? - AssetVisibilityEnum.timeline, - width: reader.readIntOrNull(offsets[21]), - ); - return object; -} - -P _assetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (reader.readDateTime(offset)) as P; - case 3: - return (reader.readDateTime(offset)) as P; - case 4: - return (reader.readString(offset)) as P; - case 5: - return (reader.readIntOrNull(offset)) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 8: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 9: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 10: - return (reader.readStringOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readLong(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - case 17: - return (reader.readStringOrNull(offset)) as P; - case 18: - return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetType.other) - as P; - case 19: - return (reader.readDateTime(offset)) as P; - case 20: - return (_AssetvisibilityValueEnumMap[reader.readByteOrNull(offset)] ?? - AssetVisibilityEnum.timeline) - as P; - case 21: - return (reader.readIntOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _AssettypeEnumValueMap = {'other': 0, 'image': 1, 'video': 2, 'audio': 3}; -const _AssettypeValueEnumMap = { - 0: AssetType.other, - 1: AssetType.image, - 2: AssetType.video, - 3: AssetType.audio, -}; -const _AssetvisibilityEnumValueMap = { - 'timeline': 0, - 'hidden': 1, - 'archive': 2, - 'locked': 3, -}; -const _AssetvisibilityValueEnumMap = { - 0: AssetVisibilityEnum.timeline, - 1: AssetVisibilityEnum.hidden, - 2: AssetVisibilityEnum.archive, - 3: AssetVisibilityEnum.locked, -}; - -Id _assetGetId(Asset object) { - return object.id; -} - -List> _assetGetLinks(Asset object) { - return []; -} - -void _assetAttach(IsarCollection col, Id id, Asset object) { - object.id = id; -} - -extension AssetByIndex on IsarCollection { - Future getByOwnerIdChecksum(int ownerId, String checksum) { - return getByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - Asset? getByOwnerIdChecksumSync(int ownerId, String checksum) { - return getByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future deleteByOwnerIdChecksum(int ownerId, String checksum) { - return deleteByIndex(r'ownerId_checksum', [ownerId, checksum]); - } - - bool deleteByOwnerIdChecksumSync(int ownerId, String checksum) { - return deleteByIndexSync(r'ownerId_checksum', [ownerId, checksum]); - } - - Future> getAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndex(r'ownerId_checksum', values); - } - - List getAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return getAllByIndexSync(r'ownerId_checksum', values); - } - - Future deleteAllByOwnerIdChecksum( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndex(r'ownerId_checksum', values); - } - - int deleteAllByOwnerIdChecksumSync( - List ownerIdValues, - List checksumValues, - ) { - final len = ownerIdValues.length; - assert( - checksumValues.length == len, - 'All index values must have the same length', - ); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([ownerIdValues[i], checksumValues[i]]); - } - - return deleteAllByIndexSync(r'ownerId_checksum', values); - } - - Future putByOwnerIdChecksum(Asset object) { - return putByIndex(r'ownerId_checksum', object); - } - - Id putByOwnerIdChecksumSync(Asset object, {bool saveLinks = true}) { - return putByIndexSync(r'ownerId_checksum', object, saveLinks: saveLinks); - } - - Future> putAllByOwnerIdChecksum(List objects) { - return putAllByIndex(r'ownerId_checksum', objects); - } - - List putAllByOwnerIdChecksumSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync( - r'ownerId_checksum', - objects, - saveLinks: saveLinks, - ); - } -} - -extension AssetQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension AssetQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [null]), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'remoteId', value: [remoteId]), - ); - }); - } - - QueryBuilder remoteIdNotEqualTo( - String? remoteId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [remoteId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'remoteId', - lower: [], - upper: [remoteId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [null]), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [null], - includeLower: false, - upper: [], - ), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'localId', value: [localId]), - ); - }); - } - - QueryBuilder localIdNotEqualTo( - String? localId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [localId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'localId', - lower: [], - upper: [localId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId], - ), - ); - }); - } - - QueryBuilder ownerIdNotEqualToAnyChecksum( - int ownerId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder ownerIdGreaterThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - includeLower: include, - upper: [], - ), - ); - }); - } - - QueryBuilder ownerIdLessThanAnyChecksum( - int ownerId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [], - upper: [ownerId], - includeUpper: include, - ), - ); - }); - } - - QueryBuilder ownerIdBetweenAnyChecksum( - int lowerOwnerId, - int upperOwnerId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [lowerOwnerId], - includeLower: includeLower, - upper: [upperOwnerId], - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder ownerIdChecksumEqualTo( - int ownerId, - String checksum, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo( - indexName: r'ownerId_checksum', - value: [ownerId, checksum], - ), - ); - }); - } - - QueryBuilder - ownerIdEqualToChecksumNotEqualTo(int ownerId, String checksum) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId, checksum], - includeLower: false, - upper: [ownerId], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'ownerId_checksum', - lower: [ownerId], - upper: [ownerId, checksum], - includeUpper: false, - ), - ); - } - }); - } -} - -extension AssetQueryFilter on QueryBuilder { - QueryBuilder checksumEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'checksum', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'checksum', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'checksum', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder checksumIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder checksumIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'checksum', value: ''), - ); - }); - } - - QueryBuilder durationInSecondsEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'durationInSeconds', value: value), - ); - }); - } - - QueryBuilder - durationInSecondsGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'durationInSeconds', - value: value, - ), - ); - }); - } - - QueryBuilder durationInSecondsBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'durationInSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileCreatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileCreatedAt', value: value), - ); - }); - } - - QueryBuilder fileCreatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileCreatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileCreatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileCreatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileModifiedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileModifiedAt', value: value), - ); - }); - } - - QueryBuilder fileModifiedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileModifiedAt', - value: value, - ), - ); - }); - } - - QueryBuilder fileModifiedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileModifiedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder fileNameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'fileName', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'fileName', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder fileNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder fileNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'fileName', value: ''), - ); - }); - } - - QueryBuilder heightIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'height'), - ); - }); - } - - QueryBuilder heightIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'height'), - ); - }); - } - - QueryBuilder heightEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'height', value: value), - ); - }); - } - - QueryBuilder heightGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'height', - value: value, - ), - ); - }); - } - - QueryBuilder heightBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'height', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isArchivedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isArchived', value: value), - ); - }); - } - - QueryBuilder isFavoriteEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isFavorite', value: value), - ); - }); - } - - QueryBuilder isOfflineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isOffline', value: value), - ); - }); - } - - QueryBuilder isTrashedEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isTrashed', value: value), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'livePhotoVideoId'), - ); - }); - } - - QueryBuilder livePhotoVideoIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'livePhotoVideoId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'livePhotoVideoId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'livePhotoVideoId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder livePhotoVideoIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder - livePhotoVideoIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'livePhotoVideoId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'localId'), - ); - }); - } - - QueryBuilder localIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'localId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'localId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'localId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder localIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder localIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'localId', value: ''), - ); - }); - } - - QueryBuilder ownerIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'ownerId', value: value), - ); - }); - } - - QueryBuilder ownerIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'ownerId', - value: value, - ), - ); - }); - } - - QueryBuilder ownerIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'ownerId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder remoteIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'remoteId'), - ); - }); - } - - QueryBuilder remoteIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'remoteId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'remoteId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'remoteId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder remoteIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder remoteIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'remoteId', value: ''), - ); - }); - } - - QueryBuilder stackCountEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackCount', value: value), - ); - }); - } - - QueryBuilder stackCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackCount', - value: value, - ), - ); - }); - } - - QueryBuilder stackCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder stackIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackId'), - ); - }); - } - - QueryBuilder stackIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder stackIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'stackId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'stackPrimaryAssetId'), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'stackPrimaryAssetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'stackPrimaryAssetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stackPrimaryAssetIdMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'stackPrimaryAssetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'stackPrimaryAssetId', value: ''), - ); - }); - } - - QueryBuilder - stackPrimaryAssetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - property: r'stackPrimaryAssetId', - value: '', - ), - ); - }); - } - - QueryBuilder thumbhashIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'thumbhash'), - ); - }); - } - - QueryBuilder thumbhashEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'thumbhash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'thumbhash', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'thumbhash', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbhashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder thumbhashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'thumbhash', value: ''), - ); - }); - } - - QueryBuilder typeEqualTo( - AssetType value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'type', value: value), - ); - }); - } - - QueryBuilder typeGreaterThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeLessThan( - AssetType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'type', - value: value, - ), - ); - }); - } - - QueryBuilder typeBetween( - AssetType lower, - AssetType upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'type', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder visibilityEqualTo( - AssetVisibilityEnum value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'visibility', value: value), - ); - }); - } - - QueryBuilder visibilityGreaterThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityLessThan( - AssetVisibilityEnum value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'visibility', - value: value, - ), - ); - }); - } - - QueryBuilder visibilityBetween( - AssetVisibilityEnum lower, - AssetVisibilityEnum upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'visibility', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder widthIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'width'), - ); - }); - } - - QueryBuilder widthIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'width'), - ); - }); - } - - QueryBuilder widthEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'width', value: value), - ); - }); - } - - QueryBuilder widthGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'width', - value: value, - ), - ); - }); - } - - QueryBuilder widthBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'width', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension AssetQueryObject on QueryBuilder {} - -extension AssetQueryLinks on QueryBuilder {} - -extension AssetQuerySortBy on QueryBuilder { - QueryBuilder sortByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder sortByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder sortByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder sortByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder sortByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder sortByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder sortByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder sortByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder sortByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder sortByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder sortByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder sortByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder sortByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder sortByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder sortByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder sortByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder sortByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder sortByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder sortByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder sortByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder sortByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder sortByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder sortByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder sortByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder sortByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder sortByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder sortByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder sortByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder sortByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder sortByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder sortByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder sortByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder sortByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder sortByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder sortByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder sortByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder sortByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder sortByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder sortByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder sortByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQuerySortThenBy on QueryBuilder { - QueryBuilder thenByChecksum() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.asc); - }); - } - - QueryBuilder thenByChecksumDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'checksum', Sort.desc); - }); - } - - QueryBuilder thenByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.asc); - }); - } - - QueryBuilder thenByDurationInSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'durationInSeconds', Sort.desc); - }); - } - - QueryBuilder thenByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileCreatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileCreatedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.asc); - }); - } - - QueryBuilder thenByFileModifiedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileModifiedAt', Sort.desc); - }); - } - - QueryBuilder thenByFileName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.asc); - }); - } - - QueryBuilder thenByFileNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileName', Sort.desc); - }); - } - - QueryBuilder thenByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.asc); - }); - } - - QueryBuilder thenByHeightDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'height', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.asc); - }); - } - - QueryBuilder thenByIsArchivedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isArchived', Sort.desc); - }); - } - - QueryBuilder thenByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.asc); - }); - } - - QueryBuilder thenByIsFavoriteDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isFavorite', Sort.desc); - }); - } - - QueryBuilder thenByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder thenByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - - QueryBuilder thenByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.asc); - }); - } - - QueryBuilder thenByIsTrashedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isTrashed', Sort.desc); - }); - } - - QueryBuilder thenByLivePhotoVideoId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.asc); - }); - } - - QueryBuilder thenByLivePhotoVideoIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'livePhotoVideoId', Sort.desc); - }); - } - - QueryBuilder thenByLocalId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.asc); - }); - } - - QueryBuilder thenByLocalIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'localId', Sort.desc); - }); - } - - QueryBuilder thenByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.asc); - }); - } - - QueryBuilder thenByOwnerIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'ownerId', Sort.desc); - }); - } - - QueryBuilder thenByRemoteId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.asc); - }); - } - - QueryBuilder thenByRemoteIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'remoteId', Sort.desc); - }); - } - - QueryBuilder thenByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.asc); - }); - } - - QueryBuilder thenByStackCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackCount', Sort.desc); - }); - } - - QueryBuilder thenByStackId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.asc); - }); - } - - QueryBuilder thenByStackIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackId', Sort.desc); - }); - } - - QueryBuilder thenByStackPrimaryAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); - }); - } - - QueryBuilder thenByStackPrimaryAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); - }); - } - - QueryBuilder thenByThumbhash() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.asc); - }); - } - - QueryBuilder thenByThumbhashDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'thumbhash', Sort.desc); - }); - } - - QueryBuilder thenByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder thenByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } - - QueryBuilder thenByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.asc); - }); - } - - QueryBuilder thenByVisibilityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'visibility', Sort.desc); - }); - } - - QueryBuilder thenByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.asc); - }); - } - - QueryBuilder thenByWidthDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'width', Sort.desc); - }); - } -} - -extension AssetQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByChecksum({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'checksum', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDurationInSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'durationInSeconds'); - }); - } - - QueryBuilder distinctByFileCreatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileCreatedAt'); - }); - } - - QueryBuilder distinctByFileModifiedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileModifiedAt'); - }); - } - - QueryBuilder distinctByFileName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByHeight() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'height'); - }); - } - - QueryBuilder distinctByIsArchived() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isArchived'); - }); - } - - QueryBuilder distinctByIsFavorite() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isFavorite'); - }); - } - - QueryBuilder distinctByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isOffline'); - }); - } - - QueryBuilder distinctByIsTrashed() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isTrashed'); - }); - } - - QueryBuilder distinctByLivePhotoVideoId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'livePhotoVideoId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByLocalId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'localId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOwnerId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'ownerId'); - }); - } - - QueryBuilder distinctByRemoteId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'remoteId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackCount'); - }); - } - - QueryBuilder distinctByStackId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByStackPrimaryAssetId({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'stackPrimaryAssetId', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByThumbhash({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByType() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'type'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } - - QueryBuilder distinctByVisibility() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'visibility'); - }); - } - - QueryBuilder distinctByWidth() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'width'); - }); - } -} - -extension AssetQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder checksumProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'checksum'); - }); - } - - QueryBuilder durationInSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'durationInSeconds'); - }); - } - - QueryBuilder fileCreatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileCreatedAt'); - }); - } - - QueryBuilder fileModifiedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileModifiedAt'); - }); - } - - QueryBuilder fileNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileName'); - }); - } - - QueryBuilder heightProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'height'); - }); - } - - QueryBuilder isArchivedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isArchived'); - }); - } - - QueryBuilder isFavoriteProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isFavorite'); - }); - } - - QueryBuilder isOfflineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isOffline'); - }); - } - - QueryBuilder isTrashedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isTrashed'); - }); - } - - QueryBuilder livePhotoVideoIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'livePhotoVideoId'); - }); - } - - QueryBuilder localIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'localId'); - }); - } - - QueryBuilder ownerIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'ownerId'); - }); - } - - QueryBuilder remoteIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'remoteId'); - }); - } - - QueryBuilder stackCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackCount'); - }); - } - - QueryBuilder stackIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackId'); - }); - } - - QueryBuilder stackPrimaryAssetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackPrimaryAssetId'); - }); - } - - QueryBuilder thumbhashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'thumbhash'); - }); - } - - QueryBuilder typeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'type'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } - - QueryBuilder - visibilityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'visibility'); - }); - } - - QueryBuilder widthProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'width'); - }); - } -} diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart deleted file mode 100644 index ad2a5d6718..0000000000 --- a/mobile/lib/entities/backup_album.entity.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'backup_album.entity.g.dart'; - -@Collection(inheritance: false) -class BackupAlbum { - String id; - DateTime lastBackup; - @Enumerated(EnumType.ordinal) - BackupSelection selection; - - BackupAlbum(this.id, this.lastBackup, this.selection); - - Id get isarId => fastHash(id); - - BackupAlbum copyWith({String? id, DateTime? lastBackup, BackupSelection? selection}) { - return BackupAlbum(id ?? this.id, lastBackup ?? this.lastBackup, selection ?? this.selection); - } -} - -enum BackupSelection { none, select, exclude } diff --git a/mobile/lib/entities/backup_album.entity.g.dart b/mobile/lib/entities/backup_album.entity.g.dart deleted file mode 100644 index 583aa55c4d..0000000000 --- a/mobile/lib/entities/backup_album.entity.g.dart +++ /dev/null @@ -1,679 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_album.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetBackupAlbumCollection on Isar { - IsarCollection get backupAlbums => this.collection(); -} - -const BackupAlbumSchema = CollectionSchema( - name: r'BackupAlbum', - id: 8308487201128361847, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - r'lastBackup': PropertySchema( - id: 1, - name: r'lastBackup', - type: IsarType.dateTime, - ), - r'selection': PropertySchema( - id: 2, - name: r'selection', - type: IsarType.byte, - enumMap: _BackupAlbumselectionEnumValueMap, - ), - }, - - estimateSize: _backupAlbumEstimateSize, - serialize: _backupAlbumSerialize, - deserialize: _backupAlbumDeserialize, - deserializeProp: _backupAlbumDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _backupAlbumGetId, - getLinks: _backupAlbumGetLinks, - attach: _backupAlbumAttach, - version: '3.3.0-dev.3', -); - -int _backupAlbumEstimateSize( - BackupAlbum object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _backupAlbumSerialize( - BackupAlbum object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); - writer.writeDateTime(offsets[1], object.lastBackup); - writer.writeByte(offsets[2], object.selection.index); -} - -BackupAlbum _backupAlbumDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = BackupAlbum( - reader.readString(offsets[0]), - reader.readDateTime(offsets[1]), - _BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - BackupSelection.none, - ); - return object; -} - -P _backupAlbumDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (_BackupAlbumselectionValueEnumMap[reader.readByteOrNull( - offset, - )] ?? - BackupSelection.none) - as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _BackupAlbumselectionEnumValueMap = { - 'none': 0, - 'select': 1, - 'exclude': 2, -}; -const _BackupAlbumselectionValueEnumMap = { - 0: BackupSelection.none, - 1: BackupSelection.select, - 2: BackupSelection.exclude, -}; - -Id _backupAlbumGetId(BackupAlbum object) { - return object.isarId; -} - -List> _backupAlbumGetLinks(BackupAlbum object) { - return []; -} - -void _backupAlbumAttach( - IsarCollection col, - Id id, - BackupAlbum object, -) {} - -extension BackupAlbumQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension BackupAlbumQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - lastBackupEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lastBackup', value: value), - ); - }); - } - - QueryBuilder - lastBackupGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lastBackup', - value: value, - ), - ); - }); - } - - QueryBuilder - lastBackupBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lastBackup', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - selectionEqualTo(BackupSelection value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'selection', value: value), - ); - }); - } - - QueryBuilder - selectionGreaterThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionLessThan(BackupSelection value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'selection', - value: value, - ), - ); - }); - } - - QueryBuilder - selectionBetween( - BackupSelection lower, - BackupSelection upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'selection', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension BackupAlbumQueryObject - on QueryBuilder {} - -extension BackupAlbumQueryLinks - on QueryBuilder {} - -extension BackupAlbumQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder sortByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder sortBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder sortBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.asc); - }); - } - - QueryBuilder thenByLastBackupDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastBackup', Sort.desc); - }); - } - - QueryBuilder thenBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.asc); - }); - } - - QueryBuilder thenBySelectionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'selection', Sort.desc); - }); - } -} - -extension BackupAlbumQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLastBackup() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastBackup'); - }); - } - - QueryBuilder distinctBySelection() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'selection'); - }); - } -} - -extension BackupAlbumQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder lastBackupProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastBackup'); - }); - } - - QueryBuilder - selectionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'selection'); - }); - } -} diff --git a/mobile/lib/entities/device_asset.entity.dart b/mobile/lib/entities/device_asset.entity.dart deleted file mode 100644 index 0973dd4ff8..0000000000 --- a/mobile/lib/entities/device_asset.entity.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:isar/isar.dart'; - -class DeviceAsset { - DeviceAsset({required this.hash}); - - @Index(unique: false, type: IndexType.hash) - List hash; -} diff --git a/mobile/lib/entities/duplicated_asset.entity.dart b/mobile/lib/entities/duplicated_asset.entity.dart deleted file mode 100644 index 9368dc1a52..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'duplicated_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DuplicatedAsset { - String id; - DuplicatedAsset(this.id); - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/duplicated_asset.entity.g.dart b/mobile/lib/entities/duplicated_asset.entity.g.dart deleted file mode 100644 index 80d2f344e6..0000000000 --- a/mobile/lib/entities/duplicated_asset.entity.g.dart +++ /dev/null @@ -1,444 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'duplicated_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDuplicatedAssetCollection on Isar { - IsarCollection get duplicatedAssets => this.collection(); -} - -const DuplicatedAssetSchema = CollectionSchema( - name: r'DuplicatedAsset', - id: -2679334728174694496, - properties: { - r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string), - }, - - estimateSize: _duplicatedAssetEstimateSize, - serialize: _duplicatedAssetSerialize, - deserialize: _duplicatedAssetDeserialize, - deserializeProp: _duplicatedAssetDeserializeProp, - idName: r'isarId', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _duplicatedAssetGetId, - getLinks: _duplicatedAssetGetLinks, - attach: _duplicatedAssetAttach, - version: '3.3.0-dev.3', -); - -int _duplicatedAssetEstimateSize( - DuplicatedAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _duplicatedAssetSerialize( - DuplicatedAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.id); -} - -DuplicatedAsset _duplicatedAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DuplicatedAsset(reader.readString(offsets[0])); - return object; -} - -P _duplicatedAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _duplicatedAssetGetId(DuplicatedAsset object) { - return object.isarId; -} - -List> _duplicatedAssetGetLinks(DuplicatedAsset object) { - return []; -} - -void _duplicatedAssetAttach( - IsarCollection col, - Id id, - DuplicatedAsset object, -) {} - -extension DuplicatedAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DuplicatedAssetQueryWhere - on QueryBuilder { - QueryBuilder - isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryFilter - on QueryBuilder { - QueryBuilder - idEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DuplicatedAssetQueryObject - on QueryBuilder {} - -extension DuplicatedAssetQueryLinks - on QueryBuilder {} - -extension DuplicatedAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension DuplicatedAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension DuplicatedAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension DuplicatedAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/etag.entity.dart b/mobile/lib/entities/etag.entity.dart deleted file mode 100644 index 3b8ef39c61..0000000000 --- a/mobile/lib/entities/etag.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'etag.entity.g.dart'; - -@Collection(inheritance: false) -class ETag { - ETag({required this.id, this.assetCount, this.time}); - Id get isarId => fastHash(id); - @Index(unique: true, replace: true, type: IndexType.hash) - String id; - int? assetCount; - DateTime? time; -} diff --git a/mobile/lib/entities/etag.entity.g.dart b/mobile/lib/entities/etag.entity.g.dart deleted file mode 100644 index 03b4ea9918..0000000000 --- a/mobile/lib/entities/etag.entity.g.dart +++ /dev/null @@ -1,796 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'etag.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetETagCollection on Isar { - IsarCollection get eTags => this.collection(); -} - -const ETagSchema = CollectionSchema( - name: r'ETag', - id: -644290296585643859, - properties: { - r'assetCount': PropertySchema( - id: 0, - name: r'assetCount', - type: IsarType.long, - ), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - r'time': PropertySchema(id: 2, name: r'time', type: IsarType.dateTime), - }, - - estimateSize: _eTagEstimateSize, - serialize: _eTagSerialize, - deserialize: _eTagDeserialize, - deserializeProp: _eTagDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _eTagGetId, - getLinks: _eTagGetLinks, - attach: _eTagAttach, - version: '3.3.0-dev.3', -); - -int _eTagEstimateSize( - ETag object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _eTagSerialize( - ETag object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.assetCount); - writer.writeString(offsets[1], object.id); - writer.writeDateTime(offsets[2], object.time); -} - -ETag _eTagDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ETag( - assetCount: reader.readLongOrNull(offsets[0]), - id: reader.readString(offsets[1]), - time: reader.readDateTimeOrNull(offsets[2]), - ); - return object; -} - -P _eTagDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _eTagGetId(ETag object) { - return object.isarId; -} - -List> _eTagGetLinks(ETag object) { - return []; -} - -void _eTagAttach(IsarCollection col, Id id, ETag object) {} - -extension ETagByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - ETag? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(ETag object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(ETag object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension ETagQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ETagQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension ETagQueryFilter on QueryBuilder { - QueryBuilder assetCountIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'assetCount'), - ); - }); - } - - QueryBuilder assetCountEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetCount', value: value), - ); - }); - } - - QueryBuilder assetCountGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetCount', - value: value, - ), - ); - }); - } - - QueryBuilder assetCountBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder timeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'time'), - ); - }); - } - - QueryBuilder timeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'time'), - ); - }); - } - - QueryBuilder timeEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'time', value: value), - ); - }); - } - - QueryBuilder timeGreaterThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeLessThan( - DateTime? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'time', - value: value, - ), - ); - }); - } - - QueryBuilder timeBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'time', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ETagQueryObject on QueryBuilder {} - -extension ETagQueryLinks on QueryBuilder {} - -extension ETagQuerySortBy on QueryBuilder { - QueryBuilder sortByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder sortByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder sortByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.asc); - }); - } - - QueryBuilder thenByAssetCountDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetCount', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder thenByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } -} - -extension ETagQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAssetCount() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetCount'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'time'); - }); - } -} - -extension ETagQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder assetCountProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetCount'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder timeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'time'); - }); - } -} diff --git a/mobile/lib/entities/ios_device_asset.entity.dart b/mobile/lib/entities/ios_device_asset.entity.dart deleted file mode 100644 index dfd0a660f8..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/entities/device_asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'ios_device_asset.entity.g.dart'; - -@Collection() -class IOSDeviceAsset extends DeviceAsset { - IOSDeviceAsset({required this.id, required super.hash}); - - @Index(replace: true, unique: true, type: IndexType.hash) - String id; - Id get isarId => fastHash(id); -} diff --git a/mobile/lib/entities/ios_device_asset.entity.g.dart b/mobile/lib/entities/ios_device_asset.entity.g.dart deleted file mode 100644 index 252fe127bb..0000000000 --- a/mobile/lib/entities/ios_device_asset.entity.g.dart +++ /dev/null @@ -1,766 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ios_device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetIOSDeviceAssetCollection on Isar { - IsarCollection get iOSDeviceAssets => this.collection(); -} - -const IOSDeviceAssetSchema = CollectionSchema( - name: r'IOSDeviceAsset', - id: -1671546753821948030, - properties: { - r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList), - r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string), - }, - - estimateSize: _iOSDeviceAssetEstimateSize, - serialize: _iOSDeviceAssetSerialize, - deserialize: _iOSDeviceAssetDeserialize, - deserializeProp: _iOSDeviceAssetDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _iOSDeviceAssetGetId, - getLinks: _iOSDeviceAssetGetLinks, - attach: _iOSDeviceAssetAttach, - version: '3.3.0-dev.3', -); - -int _iOSDeviceAssetEstimateSize( - IOSDeviceAsset object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.hash.length; - bytesCount += 3 + object.id.length * 3; - return bytesCount; -} - -void _iOSDeviceAssetSerialize( - IOSDeviceAsset object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.hash); - writer.writeString(offsets[1], object.id); -} - -IOSDeviceAsset _iOSDeviceAssetDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = IOSDeviceAsset( - hash: reader.readByteList(offsets[0]) ?? [], - id: reader.readString(offsets[1]), - ); - return object; -} - -P _iOSDeviceAssetDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - case 1: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _iOSDeviceAssetGetId(IOSDeviceAsset object) { - return object.isarId; -} - -List> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) { - return []; -} - -void _iOSDeviceAssetAttach( - IsarCollection col, - Id id, - IOSDeviceAsset object, -) {} - -extension IOSDeviceAssetByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - IOSDeviceAsset? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(IOSDeviceAsset object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension IOSDeviceAssetQueryWhereSort - on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension IOSDeviceAssetQueryWhere - on QueryBuilder { - QueryBuilder isarIdEqualTo( - Id isarId, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder - isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder - isarIdGreaterThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id isarId, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo( - String id, - ) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder hashEqualTo( - List hash, - ) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension IOSDeviceAssetQueryFilter - on QueryBuilder { - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idLessThan(String value, {bool include = false, bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder - isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder - isarIdGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder - isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension IOSDeviceAssetQueryObject - on QueryBuilder {} - -extension IOSDeviceAssetQueryLinks - on QueryBuilder {} - -extension IOSDeviceAssetQuerySortBy - on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder - thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } -} - -extension IOSDeviceAssetQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } -} - -extension IOSDeviceAssetQueryProperty - on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } -} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 7b59e119d6..17ad88cee9 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -1,38 +1,4 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; // ignore: non_constant_identifier_names final Store = StoreService.I; - -class SSLClientCertStoreVal { - final Uint8List data; - final String? password; - - const SSLClientCertStoreVal(this.data, this.password); - - Future save() async { - final b64Str = base64Encode(data); - await Store.put(StoreKey.sslClientCertData, b64Str); - if (password != null) { - await Store.put(StoreKey.sslClientPasswd, password!); - } - } - - static SSLClientCertStoreVal? load() { - final b64Str = Store.tryGet(StoreKey.sslClientCertData); - if (b64Str == null) { - return null; - } - final Uint8List certData = base64Decode(b64Str); - final passwd = Store.tryGet(StoreKey.sslClientPasswd); - return SSLClientCertStoreVal(certData, passwd); - } - - static Future delete() async { - await Store.delete(StoreKey.sslClientCertData); - await Store.delete(StoreKey.sslClientPasswd); - } -} diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index a8ca7ef2aa..6e8101bd04 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -1,17 +1,72 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/timezone.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; +import 'package:openapi/api.dart' as api; -extension TZExtension on Asset { - /// Returns the created time of the asset from the exif info (if available) or from - /// the fileCreatedAt field, adjusted to the timezone value from the exif info along with - /// the timezone offset in [Duration] - (DateTime, Duration) getTZAdjustedTimeAndOffset() { - DateTime dt = fileCreatedAt.toLocal(); +extension DTOToAsset on api.AssetResponseDto { + RemoteAsset toDto() { + return RemoteAsset( + id: id, + name: originalFileName, + checksum: checksum, + createdAt: fileCreatedAt, + updatedAt: updatedAt, + ownerId: ownerId, + visibility: visibility.toAssetVisibility(), + durationInSeconds: duration?.toDuration()?.inSeconds ?? 0, + height: height?.toInt(), + width: width?.toInt(), + isFavorite: isFavorite, + livePhotoVideoId: livePhotoVideoId, + thumbHash: thumbhash, + localId: null, + type: type.toAssetType(), + stackId: stack?.id, + isEdited: isEdited, + ); + } - if (exifInfo?.dateTimeOriginal != null) { - return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone); - } - - return (dt, dt.timeZoneOffset); + RemoteAssetExif toDtoWithExif() { + return RemoteAssetExif( + id: id, + name: originalFileName, + checksum: checksum, + createdAt: fileCreatedAt, + updatedAt: updatedAt, + ownerId: ownerId, + visibility: visibility.toAssetVisibility(), + durationInSeconds: duration?.toDuration()?.inSeconds ?? 0, + height: height?.toInt(), + width: width?.toInt(), + isFavorite: isFavorite, + livePhotoVideoId: livePhotoVideoId, + thumbHash: thumbhash, + localId: null, + type: type.toAssetType(), + stackId: stack?.id, + isEdited: isEdited, + exifInfo: exifInfo != null ? ExifDtoConverter.fromDto(exifInfo!) : const ExifInfo(), + ); } } + +extension on api.AssetVisibility { + AssetVisibility toAssetVisibility() => switch (this) { + api.AssetVisibility.timeline => AssetVisibility.timeline, + api.AssetVisibility.hidden => AssetVisibility.hidden, + api.AssetVisibility.archive => AssetVisibility.archive, + api.AssetVisibility.locked => AssetVisibility.locked, + _ => AssetVisibility.timeline, + }; +} + +extension on api.AssetTypeEnum { + AssetType toAssetType() => switch (this) { + api.AssetTypeEnum.IMAGE => AssetType.image, + api.AssetTypeEnum.VIDEO => AssetType.video, + api.AssetTypeEnum.AUDIO => AssetType.audio, + api.AssetTypeEnum.OTHER => AssetType.other, + _ => throw Exception('Unknown AssetType value: $this'), + }; +} diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index 541db7ccaf..b861eb0570 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -1,9 +1,6 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hash.dart'; extension ListExtension on List { List uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) { @@ -40,31 +37,6 @@ extension IntListExtension on Iterable { } } -extension AssetListExtension on Iterable { - /// Returns the assets that are already available in the Immich server - Iterable remoteOnly({void Function()? errorCallback}) { - final bool onlyRemote = every((e) => e.isRemote); - if (!onlyRemote) { - if (errorCallback != null) errorCallback(); - return where((a) => a.isRemote); - } - return this; - } - - /// Returns the assets that are owned by the user passed to the [owner] param - /// If [owner] is null, an empty list is returned - Iterable ownedOnly(UserDto? owner, {void Function()? errorCallback}) { - if (owner == null) return []; - final isarUserId = fastHash(owner.id); - final bool onlyOwned = every((e) => e.ownerId == isarUserId); - if (!onlyOwned) { - if (errorCallback != null) errorCallback(); - return where((a) => a.ownerId == isarUserId); - } - return this; - } -} - extension SortedByProperty on Iterable { Iterable sortedByField(Comparable Function(T e) key) { return sorted((a, b) => key(a).compareTo(key(b))); diff --git a/mobile/lib/extensions/object_extensions.dart b/mobile/lib/extensions/object_extensions.dart new file mode 100644 index 0000000000..4e76532137 --- /dev/null +++ b/mobile/lib/extensions/object_extensions.dart @@ -0,0 +1,3 @@ +extension Let on T { + R let(R Function(T) transform) => transform(this); +} diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index 7677f3cbd8..b01203a90c 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:intl/message_format.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:intl/message_format.dart'; extension StringTranslateExtension on String { String t({BuildContext? context, Map? args}) { diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 22d059bdb4..87a05ab8fe 100644 --- a/mobile/lib/infrastructure/entities/asset_edit.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/extensions/object_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)') class AssetEditEntity extends Table with DriftDefaultsMixin { @@ -27,7 +29,12 @@ final JsonTypeConverter2, Uint8List, Object?> editParameter ); extension AssetEditEntityDataDomainEx on AssetEditEntityData { - AssetEdit toDto() { - return AssetEdit(action: action, parameters: parameters); + AssetEdit? toDto() { + return switch (action) { + AssetEditAction.crop => CropParameters.fromJson(parameters)?.let(CropEdit.new), + AssetEditAction.rotate => RotateParameters.fromJson(parameters)?.let(RotateEdit.new), + AssetEditAction.mirror => MirrorParameters.fromJson(parameters)?.let(MirrorEdit.new), + AssetEditAction.other => null, + }; } } diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.dart b/mobile/lib/infrastructure/entities/device_asset.entity.dart deleted file mode 100644 index e3e4a0d4f4..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:typed_data'; - -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'device_asset.entity.g.dart'; - -@Collection(inheritance: false) -class DeviceAssetEntity { - Id get id => fastHash(assetId); - - @Index(replace: true, unique: true, type: IndexType.hash) - final String assetId; - @Index(unique: false, type: IndexType.hash) - final List hash; - final DateTime modifiedTime; - - const DeviceAssetEntity({required this.assetId, required this.hash, required this.modifiedTime}); - - DeviceAsset toModel() => DeviceAsset(assetId: assetId, hash: Uint8List.fromList(hash), modifiedTime: modifiedTime); - - static DeviceAssetEntity fromDto(DeviceAsset dto) => - DeviceAssetEntity(assetId: dto.assetId, hash: dto.hash, modifiedTime: dto.modifiedTime); -} diff --git a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart b/mobile/lib/infrastructure/entities/device_asset.entity.g.dart deleted file mode 100644 index b6c30aca6f..0000000000 --- a/mobile/lib/infrastructure/entities/device_asset.entity.g.dart +++ /dev/null @@ -1,874 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'device_asset.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDeviceAssetEntityCollection on Isar { - IsarCollection get deviceAssetEntitys => this.collection(); -} - -const DeviceAssetEntitySchema = CollectionSchema( - name: r'DeviceAssetEntity', - id: 6967030785073446271, - properties: { - r'assetId': PropertySchema(id: 0, name: r'assetId', type: IsarType.string), - r'hash': PropertySchema(id: 1, name: r'hash', type: IsarType.byteList), - r'modifiedTime': PropertySchema( - id: 2, - name: r'modifiedTime', - type: IsarType.dateTime, - ), - }, - - estimateSize: _deviceAssetEntityEstimateSize, - serialize: _deviceAssetEntitySerialize, - deserialize: _deviceAssetEntityDeserialize, - deserializeProp: _deviceAssetEntityDeserializeProp, - idName: r'id', - indexes: { - r'assetId': IndexSchema( - id: 174362542210192109, - name: r'assetId', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'assetId', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - r'hash': IndexSchema( - id: -7973251393006690288, - name: r'hash', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'hash', - type: IndexType.hash, - caseSensitive: false, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _deviceAssetEntityGetId, - getLinks: _deviceAssetEntityGetLinks, - attach: _deviceAssetEntityAttach, - version: '3.3.0-dev.3', -); - -int _deviceAssetEntityEstimateSize( - DeviceAssetEntity object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.assetId.length * 3; - bytesCount += 3 + object.hash.length; - return bytesCount; -} - -void _deviceAssetEntitySerialize( - DeviceAssetEntity object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.assetId); - writer.writeByteList(offsets[1], object.hash); - writer.writeDateTime(offsets[2], object.modifiedTime); -} - -DeviceAssetEntity _deviceAssetEntityDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DeviceAssetEntity( - assetId: reader.readString(offsets[0]), - hash: reader.readByteList(offsets[1]) ?? [], - modifiedTime: reader.readDateTime(offsets[2]), - ); - return object; -} - -P _deviceAssetEntityDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readByteList(offset) ?? []) as P; - case 2: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _deviceAssetEntityGetId(DeviceAssetEntity object) { - return object.id; -} - -List> _deviceAssetEntityGetLinks( - DeviceAssetEntity object, -) { - return []; -} - -void _deviceAssetEntityAttach( - IsarCollection col, - Id id, - DeviceAssetEntity object, -) {} - -extension DeviceAssetEntityByIndex on IsarCollection { - Future getByAssetId(String assetId) { - return getByIndex(r'assetId', [assetId]); - } - - DeviceAssetEntity? getByAssetIdSync(String assetId) { - return getByIndexSync(r'assetId', [assetId]); - } - - Future deleteByAssetId(String assetId) { - return deleteByIndex(r'assetId', [assetId]); - } - - bool deleteByAssetIdSync(String assetId) { - return deleteByIndexSync(r'assetId', [assetId]); - } - - Future> getAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndex(r'assetId', values); - } - - List getAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'assetId', values); - } - - Future deleteAllByAssetId(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'assetId', values); - } - - int deleteAllByAssetIdSync(List assetIdValues) { - final values = assetIdValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'assetId', values); - } - - Future putByAssetId(DeviceAssetEntity object) { - return putByIndex(r'assetId', object); - } - - Id putByAssetIdSync(DeviceAssetEntity object, {bool saveLinks = true}) { - return putByIndexSync(r'assetId', object, saveLinks: saveLinks); - } - - Future> putAllByAssetId(List objects) { - return putAllByIndex(r'assetId', objects); - } - - List putAllByAssetIdSync( - List objects, { - bool saveLinks = true, - }) { - return putAllByIndexSync(r'assetId', objects, saveLinks: saveLinks); - } -} - -extension DeviceAssetEntityQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DeviceAssetEntityQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - assetIdEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'assetId', value: [assetId]), - ); - }); - } - - QueryBuilder - assetIdNotEqualTo(String assetId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [assetId], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'assetId', - lower: [], - upper: [assetId], - includeUpper: false, - ), - ); - } - }); - } - - QueryBuilder - hashEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'hash', value: [hash]), - ); - }); - } - - QueryBuilder - hashNotEqualTo(List hash) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [hash], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'hash', - lower: [], - upper: [hash], - includeUpper: false, - ), - ); - } - }); - } -} - -extension DeviceAssetEntityQueryFilter - on QueryBuilder { - QueryBuilder - assetIdEqualTo(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'assetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdEndsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'assetId', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'assetId', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - assetIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - assetIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'assetId', value: ''), - ); - }); - } - - QueryBuilder - hashElementEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'hash', value: value), - ); - }); - } - - QueryBuilder - hashElementGreaterThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementLessThan(int value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'hash', - value: value, - ), - ); - }); - } - - QueryBuilder - hashElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'hash', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - hashLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, true, length, true); - }); - } - - QueryBuilder - hashIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, 0, true); - }); - } - - QueryBuilder - hashIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, false, 999999, true); - }); - } - - QueryBuilder - hashLengthLessThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', 0, true, length, include); - }); - } - - QueryBuilder - hashLengthGreaterThan(int length, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.listLength(r'hash', length, include, 999999, true); - }); - } - - QueryBuilder - hashLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'hash', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder - idGreaterThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idLessThan(Id value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder - modifiedTimeEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'modifiedTime', value: value), - ); - }); - } - - QueryBuilder - modifiedTimeGreaterThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeLessThan(DateTime value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'modifiedTime', - value: value, - ), - ); - }); - } - - QueryBuilder - modifiedTimeBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'modifiedTime', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension DeviceAssetEntityQueryObject - on QueryBuilder {} - -extension DeviceAssetEntityQueryLinks - on QueryBuilder {} - -extension DeviceAssetEntityQuerySortBy - on QueryBuilder { - QueryBuilder - sortByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - sortByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder - sortByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - sortByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByAssetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.asc); - }); - } - - QueryBuilder - thenByAssetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'assetId', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.asc); - }); - } - - QueryBuilder - thenByModifiedTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'modifiedTime', Sort.desc); - }); - } -} - -extension DeviceAssetEntityQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByAssetId({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'assetId', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByHash() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hash'); - }); - } - - QueryBuilder - distinctByModifiedTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'modifiedTime'); - }); - } -} - -extension DeviceAssetEntityQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder assetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'assetId'); - }); - } - - QueryBuilder, QQueryOperations> hashProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hash'); - }); - } - - QueryBuilder - modifiedTimeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'modifiedTime'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 77cae5dbbe..e009029ea7 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -4,96 +4,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:isar/isar.dart'; - -part 'exif.entity.g.dart'; - -/// Exif information 1:1 relation with Asset -@Collection(inheritance: false) -class ExifInfo { - final Id? id; - final int? fileSize; - final DateTime? dateTimeOriginal; - final String? timeZone; - final String? make; - final String? model; - final String? lens; - final float? f; - final float? mm; - final short? iso; - final float? exposureSeconds; - final float? lat; - final float? long; - final String? city; - final String? state; - final String? country; - final String? description; - final String? orientation; - - const ExifInfo({ - this.id, - this.fileSize, - this.dateTimeOriginal, - this.timeZone, - this.make, - this.model, - this.lens, - this.f, - this.mm, - this.iso, - this.exposureSeconds, - this.lat, - this.long, - this.city, - this.state, - this.country, - this.description, - this.orientation, - }); - - static ExifInfo fromDto(domain.ExifInfo dto) => ExifInfo( - id: dto.assetId, - fileSize: dto.fileSize, - dateTimeOriginal: dto.dateTimeOriginal, - timeZone: dto.timeZone, - make: dto.make, - model: dto.model, - lens: dto.lens, - f: dto.f, - mm: dto.mm, - iso: dto.iso?.toInt(), - exposureSeconds: dto.exposureSeconds, - lat: dto.latitude, - long: dto.longitude, - city: dto.city, - state: dto.state, - country: dto.country, - description: dto.description, - orientation: dto.orientation, - ); - - domain.ExifInfo toDto() => domain.ExifInfo( - assetId: id, - fileSize: fileSize, - description: description, - orientation: orientation, - timeZone: timeZone, - dateTimeOriginal: dateTimeOriginal, - isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), - latitude: lat, - longitude: long, - city: city, - state: state, - country: country, - make: make, - model: model, - lens: lens, - f: f, - mm: mm, - iso: iso?.toInt(), - exposureSeconds: exposureSeconds, - ); -} @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)') class RemoteExifEntity extends Table with DriftDefaultsMixin { @@ -152,6 +62,8 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { fileSize: fileSize, dateTimeOriginal: dateTimeOriginal, rating: rating, + width: width, + height: height, timeZone: timeZone, make: make, model: model, diff --git a/mobile/lib/infrastructure/entities/exif.entity.g.dart b/mobile/lib/infrastructure/entities/exif.entity.g.dart deleted file mode 100644 index ffbfd0d8f0..0000000000 --- a/mobile/lib/infrastructure/entities/exif.entity.g.dart +++ /dev/null @@ -1,3200 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetExifInfoCollection on Isar { - IsarCollection get exifInfos => this.collection(); -} - -const ExifInfoSchema = CollectionSchema( - name: r'ExifInfo', - id: -2409260054350835217, - properties: { - r'city': PropertySchema(id: 0, name: r'city', type: IsarType.string), - r'country': PropertySchema(id: 1, name: r'country', type: IsarType.string), - r'dateTimeOriginal': PropertySchema( - id: 2, - name: r'dateTimeOriginal', - type: IsarType.dateTime, - ), - r'description': PropertySchema( - id: 3, - name: r'description', - type: IsarType.string, - ), - r'exposureSeconds': PropertySchema( - id: 4, - name: r'exposureSeconds', - type: IsarType.float, - ), - r'f': PropertySchema(id: 5, name: r'f', type: IsarType.float), - r'fileSize': PropertySchema(id: 6, name: r'fileSize', type: IsarType.long), - r'iso': PropertySchema(id: 7, name: r'iso', type: IsarType.int), - r'lat': PropertySchema(id: 8, name: r'lat', type: IsarType.float), - r'lens': PropertySchema(id: 9, name: r'lens', type: IsarType.string), - r'long': PropertySchema(id: 10, name: r'long', type: IsarType.float), - r'make': PropertySchema(id: 11, name: r'make', type: IsarType.string), - r'mm': PropertySchema(id: 12, name: r'mm', type: IsarType.float), - r'model': PropertySchema(id: 13, name: r'model', type: IsarType.string), - r'orientation': PropertySchema( - id: 14, - name: r'orientation', - type: IsarType.string, - ), - r'state': PropertySchema(id: 15, name: r'state', type: IsarType.string), - r'timeZone': PropertySchema( - id: 16, - name: r'timeZone', - type: IsarType.string, - ), - }, - - estimateSize: _exifInfoEstimateSize, - serialize: _exifInfoSerialize, - deserialize: _exifInfoDeserialize, - deserializeProp: _exifInfoDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _exifInfoGetId, - getLinks: _exifInfoGetLinks, - attach: _exifInfoAttach, - version: '3.3.0-dev.3', -); - -int _exifInfoEstimateSize( - ExifInfo object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.city; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.country; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.description; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.lens; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.make; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.model; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.orientation; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.state; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.timeZone; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _exifInfoSerialize( - ExifInfo object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.city); - writer.writeString(offsets[1], object.country); - writer.writeDateTime(offsets[2], object.dateTimeOriginal); - writer.writeString(offsets[3], object.description); - writer.writeFloat(offsets[4], object.exposureSeconds); - writer.writeFloat(offsets[5], object.f); - writer.writeLong(offsets[6], object.fileSize); - writer.writeInt(offsets[7], object.iso); - writer.writeFloat(offsets[8], object.lat); - writer.writeString(offsets[9], object.lens); - writer.writeFloat(offsets[10], object.long); - writer.writeString(offsets[11], object.make); - writer.writeFloat(offsets[12], object.mm); - writer.writeString(offsets[13], object.model); - writer.writeString(offsets[14], object.orientation); - writer.writeString(offsets[15], object.state); - writer.writeString(offsets[16], object.timeZone); -} - -ExifInfo _exifInfoDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ExifInfo( - city: reader.readStringOrNull(offsets[0]), - country: reader.readStringOrNull(offsets[1]), - dateTimeOriginal: reader.readDateTimeOrNull(offsets[2]), - description: reader.readStringOrNull(offsets[3]), - exposureSeconds: reader.readFloatOrNull(offsets[4]), - f: reader.readFloatOrNull(offsets[5]), - fileSize: reader.readLongOrNull(offsets[6]), - id: id, - iso: reader.readIntOrNull(offsets[7]), - lat: reader.readFloatOrNull(offsets[8]), - lens: reader.readStringOrNull(offsets[9]), - long: reader.readFloatOrNull(offsets[10]), - make: reader.readStringOrNull(offsets[11]), - mm: reader.readFloatOrNull(offsets[12]), - model: reader.readStringOrNull(offsets[13]), - orientation: reader.readStringOrNull(offsets[14]), - state: reader.readStringOrNull(offsets[15]), - timeZone: reader.readStringOrNull(offsets[16]), - ); - return object; -} - -P _exifInfoDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readStringOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - case 2: - return (reader.readDateTimeOrNull(offset)) as P; - case 3: - return (reader.readStringOrNull(offset)) as P; - case 4: - return (reader.readFloatOrNull(offset)) as P; - case 5: - return (reader.readFloatOrNull(offset)) as P; - case 6: - return (reader.readLongOrNull(offset)) as P; - case 7: - return (reader.readIntOrNull(offset)) as P; - case 8: - return (reader.readFloatOrNull(offset)) as P; - case 9: - return (reader.readStringOrNull(offset)) as P; - case 10: - return (reader.readFloatOrNull(offset)) as P; - case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: - return (reader.readFloatOrNull(offset)) as P; - case 13: - return (reader.readStringOrNull(offset)) as P; - case 14: - return (reader.readStringOrNull(offset)) as P; - case 15: - return (reader.readStringOrNull(offset)) as P; - case 16: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _exifInfoGetId(ExifInfo object) { - return object.id ?? Isar.autoIncrement; -} - -List> _exifInfoGetLinks(ExifInfo object) { - return []; -} - -void _exifInfoAttach(IsarCollection col, Id id, ExifInfo object) {} - -extension ExifInfoQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension ExifInfoQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension ExifInfoQueryFilter - on QueryBuilder { - QueryBuilder cityIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'city'), - ); - }); - } - - QueryBuilder cityIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'city'), - ); - }); - } - - QueryBuilder cityEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'city', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'city', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'city', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder cityIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'city', value: ''), - ); - }); - } - - QueryBuilder cityIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'city', value: ''), - ); - }); - } - - QueryBuilder countryIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'country'), - ); - }); - } - - QueryBuilder countryIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'country'), - ); - }); - } - - QueryBuilder countryEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'country', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'country', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'country', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder countryIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'country', value: ''), - ); - }); - } - - QueryBuilder countryIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'country', value: ''), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'dateTimeOriginal'), - ); - }); - } - - QueryBuilder - dateTimeOriginalEqualTo(DateTime? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'dateTimeOriginal', value: value), - ); - }); - } - - QueryBuilder - dateTimeOriginalGreaterThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalLessThan(DateTime? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'dateTimeOriginal', - value: value, - ), - ); - }); - } - - QueryBuilder - dateTimeOriginalBetween( - DateTime? lower, - DateTime? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'dateTimeOriginal', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder descriptionIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'description'), - ); - }); - } - - QueryBuilder - descriptionIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'description'), - ); - }); - } - - QueryBuilder descriptionEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - descriptionGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'description', value: ''), - ); - }); - } - - QueryBuilder - exposureSecondsIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'exposureSeconds'), - ); - }); - } - - QueryBuilder - exposureSecondsEqualTo(double? value, {double epsilon = Query.epsilon}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'exposureSeconds', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - exposureSecondsBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'exposureSeconds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'f'), - ); - }); - } - - QueryBuilder fIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'f'), - ); - }); - } - - QueryBuilder fEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'f', value: value, epsilon: epsilon), - ); - }); - } - - QueryBuilder fGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'f', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'f', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder fileSizeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'fileSize'), - ); - }); - } - - QueryBuilder fileSizeEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'fileSize', value: value), - ); - }); - } - - QueryBuilder fileSizeGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'fileSize', - value: value, - ), - ); - }); - } - - QueryBuilder fileSizeBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'fileSize', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'id'), - ); - }); - } - - QueryBuilder idIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'id'), - ); - }); - } - - QueryBuilder idEqualTo(Id? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id? lower, - Id? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder isoIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'iso'), - ); - }); - } - - QueryBuilder isoEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'iso', value: value), - ); - }); - } - - QueryBuilder isoGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'iso', - value: value, - ), - ); - }); - } - - QueryBuilder isoBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'iso', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder latIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lat'), - ); - }); - } - - QueryBuilder latIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lat'), - ); - }); - } - - QueryBuilder latEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lat', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder latBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder lensIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'lens'), - ); - }); - } - - QueryBuilder lensEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'lens', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'lens', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'lens', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder lensIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder lensIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'lens', value: ''), - ); - }); - } - - QueryBuilder longIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'long'), - ); - }); - } - - QueryBuilder longIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'long'), - ); - }); - } - - QueryBuilder longEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'long', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder longBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'long', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder makeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'make'), - ); - }); - } - - QueryBuilder makeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'make'), - ); - }); - } - - QueryBuilder makeEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'make', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'make', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'make', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder makeIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'make', value: ''), - ); - }); - } - - QueryBuilder makeIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'make', value: ''), - ); - }); - } - - QueryBuilder mmIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'mm'), - ); - }); - } - - QueryBuilder mmEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'mm', - value: value, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder mmBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'mm', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder modelIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'model'), - ); - }); - } - - QueryBuilder modelIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'model'), - ); - }); - } - - QueryBuilder modelEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'model', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'model', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'model', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder modelIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'model', value: ''), - ); - }); - } - - QueryBuilder modelIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'model', value: ''), - ); - }); - } - - QueryBuilder orientationIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'orientation'), - ); - }); - } - - QueryBuilder - orientationIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'orientation'), - ); - }); - } - - QueryBuilder orientationEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - orientationGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'orientation', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'orientation', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'orientation', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder orientationIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder - orientationIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'orientation', value: ''), - ); - }); - } - - QueryBuilder stateIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'state'), - ); - }); - } - - QueryBuilder stateIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'state'), - ); - }); - } - - QueryBuilder stateEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'state', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'state', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'state', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder stateIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'state', value: ''), - ); - }); - } - - QueryBuilder stateIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'state', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'timeZone'), - ); - }); - } - - QueryBuilder timeZoneEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'timeZone', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'timeZone', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'timeZone', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder timeZoneIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'timeZone', value: ''), - ); - }); - } - - QueryBuilder timeZoneIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'timeZone', value: ''), - ); - }); - } -} - -extension ExifInfoQueryObject - on QueryBuilder {} - -extension ExifInfoQueryLinks - on QueryBuilder {} - -extension ExifInfoQuerySortBy on QueryBuilder { - QueryBuilder sortByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder sortByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder sortByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder sortByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder sortByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder sortByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder sortByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder sortByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder sortByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder sortByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder sortByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder sortByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder sortByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder sortByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder sortByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder sortByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder sortByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder sortByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder sortByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder sortByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder sortByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder sortByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder sortByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder sortByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder sortByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder sortByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder sortByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder sortByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder sortByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder sortByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder sortByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder sortByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder sortByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder sortByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByCity() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.asc); - }); - } - - QueryBuilder thenByCityDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'city', Sort.desc); - }); - } - - QueryBuilder thenByCountry() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.asc); - }); - } - - QueryBuilder thenByCountryDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'country', Sort.desc); - }); - } - - QueryBuilder thenByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.asc); - }); - } - - QueryBuilder thenByDateTimeOriginalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'dateTimeOriginal', Sort.desc); - }); - } - - QueryBuilder thenByDescription() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.asc); - }); - } - - QueryBuilder thenByDescriptionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'description', Sort.desc); - }); - } - - QueryBuilder thenByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.asc); - }); - } - - QueryBuilder thenByExposureSecondsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'exposureSeconds', Sort.desc); - }); - } - - QueryBuilder thenByF() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.asc); - }); - } - - QueryBuilder thenByFDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'f', Sort.desc); - }); - } - - QueryBuilder thenByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.asc); - }); - } - - QueryBuilder thenByFileSizeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileSize', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIso() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.asc); - }); - } - - QueryBuilder thenByIsoDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'iso', Sort.desc); - }); - } - - QueryBuilder thenByLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.asc); - }); - } - - QueryBuilder thenByLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lat', Sort.desc); - }); - } - - QueryBuilder thenByLens() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.asc); - }); - } - - QueryBuilder thenByLensDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lens', Sort.desc); - }); - } - - QueryBuilder thenByLong() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.asc); - }); - } - - QueryBuilder thenByLongDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'long', Sort.desc); - }); - } - - QueryBuilder thenByMake() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.asc); - }); - } - - QueryBuilder thenByMakeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'make', Sort.desc); - }); - } - - QueryBuilder thenByMm() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.asc); - }); - } - - QueryBuilder thenByMmDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mm', Sort.desc); - }); - } - - QueryBuilder thenByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder thenByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder thenByOrientation() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.asc); - }); - } - - QueryBuilder thenByOrientationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'orientation', Sort.desc); - }); - } - - QueryBuilder thenByState() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.asc); - }); - } - - QueryBuilder thenByStateDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'state', Sort.desc); - }); - } - - QueryBuilder thenByTimeZone() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.asc); - }); - } - - QueryBuilder thenByTimeZoneDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'timeZone', Sort.desc); - }); - } -} - -extension ExifInfoQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByCity({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'city', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByCountry({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'country', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDateTimeOriginal() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'dateTimeOriginal'); - }); - } - - QueryBuilder distinctByDescription({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'description', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByExposureSeconds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'exposureSeconds'); - }); - } - - QueryBuilder distinctByF() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'f'); - }); - } - - QueryBuilder distinctByFileSize() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileSize'); - }); - } - - QueryBuilder distinctByIso() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'iso'); - }); - } - - QueryBuilder distinctByLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lat'); - }); - } - - QueryBuilder distinctByLens({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lens', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLong() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'long'); - }); - } - - QueryBuilder distinctByMake({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'make', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByMm() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mm'); - }); - } - - QueryBuilder distinctByModel({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'model', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByOrientation({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByState({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'state', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTimeZone({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'timeZone', caseSensitive: caseSensitive); - }); - } -} - -extension ExifInfoQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder cityProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'city'); - }); - } - - QueryBuilder countryProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'country'); - }); - } - - QueryBuilder - dateTimeOriginalProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'dateTimeOriginal'); - }); - } - - QueryBuilder descriptionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'description'); - }); - } - - QueryBuilder exposureSecondsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'exposureSeconds'); - }); - } - - QueryBuilder fProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'f'); - }); - } - - QueryBuilder fileSizeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileSize'); - }); - } - - QueryBuilder isoProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'iso'); - }); - } - - QueryBuilder latProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lat'); - }); - } - - QueryBuilder lensProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lens'); - }); - } - - QueryBuilder longProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'long'); - }); - } - - QueryBuilder makeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'make'); - }); - } - - QueryBuilder mmProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mm'); - }); - } - - QueryBuilder modelProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'model'); - }); - } - - QueryBuilder orientationProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'orientation'); - }); - } - - QueryBuilder stateProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'state'); - }); - } - - QueryBuilder timeZoneProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'timeZone'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart index d4b3eec84f..2de8eb713e 100644 --- a/mobile/lib/infrastructure/entities/store.entity.dart +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -1,18 +1,5 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:isar/isar.dart'; - -part 'store.entity.g.dart'; - -/// Internal class for `Store`, do not use elsewhere. -@Collection(inheritance: false) -class StoreValue { - final Id id; - final int? intValue; - final String? strValue; - - const StoreValue(this.id, {this.intValue, this.strValue}); -} class StoreEntity extends Table with DriftDefaultsMixin { IntColumn get id => integer()(); diff --git a/mobile/lib/infrastructure/entities/store.entity.g.dart b/mobile/lib/infrastructure/entities/store.entity.g.dart deleted file mode 100644 index 626c3084fe..0000000000 --- a/mobile/lib/infrastructure/entities/store.entity.g.dart +++ /dev/null @@ -1,596 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'store.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetStoreValueCollection on Isar { - IsarCollection get storeValues => this.collection(); -} - -const StoreValueSchema = CollectionSchema( - name: r'StoreValue', - id: 902899285492123510, - properties: { - r'intValue': PropertySchema(id: 0, name: r'intValue', type: IsarType.long), - r'strValue': PropertySchema( - id: 1, - name: r'strValue', - type: IsarType.string, - ), - }, - - estimateSize: _storeValueEstimateSize, - serialize: _storeValueSerialize, - deserialize: _storeValueDeserialize, - deserializeProp: _storeValueDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - - getId: _storeValueGetId, - getLinks: _storeValueGetLinks, - attach: _storeValueAttach, - version: '3.3.0-dev.3', -); - -int _storeValueEstimateSize( - StoreValue object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.strValue; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - return bytesCount; -} - -void _storeValueSerialize( - StoreValue object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.intValue); - writer.writeString(offsets[1], object.strValue); -} - -StoreValue _storeValueDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = StoreValue( - id, - intValue: reader.readLongOrNull(offsets[0]), - strValue: reader.readStringOrNull(offsets[1]), - ); - return object; -} - -P _storeValueDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readStringOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _storeValueGetId(StoreValue object) { - return object.id; -} - -List> _storeValueGetLinks(StoreValue object) { - return []; -} - -void _storeValueAttach(IsarCollection col, Id id, StoreValue object) {} - -extension StoreValueQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension StoreValueQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan( - Id id, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension StoreValueQueryFilter - on QueryBuilder { - QueryBuilder idEqualTo( - Id value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: value), - ); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder intValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'intValue'), - ); - }); - } - - QueryBuilder - intValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'intValue'), - ); - }); - } - - QueryBuilder intValueEqualTo( - int? value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'intValue', value: value), - ); - }); - } - - QueryBuilder - intValueGreaterThan(int? value, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'intValue', - value: value, - ), - ); - }); - } - - QueryBuilder intValueBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'intValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder strValueIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNull(property: r'strValue'), - ); - }); - } - - QueryBuilder - strValueIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const FilterCondition.isNotNull(property: r'strValue'), - ); - }); - } - - QueryBuilder strValueEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'strValue', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueStartsWith(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'strValue', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder strValueMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'strValue', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - strValueIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'strValue', value: ''), - ); - }); - } - - QueryBuilder - strValueIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'strValue', value: ''), - ); - }); - } -} - -extension StoreValueQueryObject - on QueryBuilder {} - -extension StoreValueQueryLinks - on QueryBuilder {} - -extension StoreValueQuerySortBy - on QueryBuilder { - QueryBuilder sortByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder sortByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder sortByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder sortByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQuerySortThenBy - on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.asc); - }); - } - - QueryBuilder thenByIntValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'intValue', Sort.desc); - }); - } - - QueryBuilder thenByStrValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.asc); - }); - } - - QueryBuilder thenByStrValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'strValue', Sort.desc); - }); - } -} - -extension StoreValueQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByIntValue() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'intValue'); - }); - } - - QueryBuilder distinctByStrValue({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'strValue', caseSensitive: caseSensitive); - }); - } -} - -extension StoreValueQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder intValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'intValue'); - }); - } - - QueryBuilder strValueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'strValue'); - }); - } -} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 667a9d6a59..8d4371672c 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,79 +1,6 @@ import 'package:drift/drift.dart' hide Index; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -part 'user.entity.g.dart'; - -@Collection(inheritance: false) -class User { - Id get isarId => fastHash(id); - @Index(unique: true, replace: false, type: IndexType.hash) - final String id; - final DateTime updatedAt; - final String email; - final String name; - final bool isPartnerSharedBy; - final bool isPartnerSharedWith; - final bool isAdmin; - final String profileImagePath; - @Enumerated(EnumType.ordinal) - final AvatarColor avatarColor; - final bool memoryEnabled; - final bool inTimeline; - final int quotaUsageInBytes; - final int quotaSizeInBytes; - - const User({ - required this.id, - required this.updatedAt, - required this.email, - required this.name, - required this.isAdmin, - this.isPartnerSharedBy = false, - this.isPartnerSharedWith = false, - this.profileImagePath = '', - this.avatarColor = AvatarColor.primary, - this.memoryEnabled = true, - this.inTimeline = false, - this.quotaUsageInBytes = 0, - this.quotaSizeInBytes = 0, - }); - - static User fromDto(UserDto dto) => User( - id: dto.id, - updatedAt: dto.updatedAt ?? DateTime(2025), - email: dto.email, - name: dto.name, - isAdmin: dto.isAdmin, - isPartnerSharedBy: dto.isPartnerSharedBy, - isPartnerSharedWith: dto.isPartnerSharedWith, - profileImagePath: dto.hasProfileImage ? "HAS_PROFILE_IMAGE" : "", - avatarColor: dto.avatarColor, - memoryEnabled: dto.memoryEnabled, - inTimeline: dto.inTimeline, - quotaUsageInBytes: dto.quotaUsageInBytes, - quotaSizeInBytes: dto.quotaSizeInBytes, - ); - - UserDto toDto() => UserDto( - id: id, - email: email, - name: name, - isAdmin: isAdmin, - updatedAt: updatedAt, - avatarColor: avatarColor, - memoryEnabled: memoryEnabled, - inTimeline: inTimeline, - isPartnerSharedBy: isPartnerSharedBy, - isPartnerSharedWith: isPartnerSharedWith, - hasProfileImage: profileImagePath.isNotEmpty, - profileChangedAt: updatedAt, - quotaUsageInBytes: quotaUsageInBytes, - quotaSizeInBytes: quotaSizeInBytes, - ); -} class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); diff --git a/mobile/lib/infrastructure/entities/user.entity.g.dart b/mobile/lib/infrastructure/entities/user.entity.g.dart deleted file mode 100644 index 7e0af41b77..0000000000 --- a/mobile/lib/infrastructure/entities/user.entity.g.dart +++ /dev/null @@ -1,1854 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.entity.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetUserCollection on Isar { - IsarCollection get users => this.collection(); -} - -const UserSchema = CollectionSchema( - name: r'User', - id: -7838171048429979076, - properties: { - r'avatarColor': PropertySchema( - id: 0, - name: r'avatarColor', - type: IsarType.byte, - enumMap: _UseravatarColorEnumValueMap, - ), - r'email': PropertySchema(id: 1, name: r'email', type: IsarType.string), - r'id': PropertySchema(id: 2, name: r'id', type: IsarType.string), - r'inTimeline': PropertySchema( - id: 3, - name: r'inTimeline', - type: IsarType.bool, - ), - r'isAdmin': PropertySchema(id: 4, name: r'isAdmin', type: IsarType.bool), - r'isPartnerSharedBy': PropertySchema( - id: 5, - name: r'isPartnerSharedBy', - type: IsarType.bool, - ), - r'isPartnerSharedWith': PropertySchema( - id: 6, - name: r'isPartnerSharedWith', - type: IsarType.bool, - ), - r'memoryEnabled': PropertySchema( - id: 7, - name: r'memoryEnabled', - type: IsarType.bool, - ), - r'name': PropertySchema(id: 8, name: r'name', type: IsarType.string), - r'profileImagePath': PropertySchema( - id: 9, - name: r'profileImagePath', - type: IsarType.string, - ), - r'quotaSizeInBytes': PropertySchema( - id: 10, - name: r'quotaSizeInBytes', - type: IsarType.long, - ), - r'quotaUsageInBytes': PropertySchema( - id: 11, - name: r'quotaUsageInBytes', - type: IsarType.long, - ), - r'updatedAt': PropertySchema( - id: 12, - name: r'updatedAt', - type: IsarType.dateTime, - ), - }, - - estimateSize: _userEstimateSize, - serialize: _userSerialize, - deserialize: _userDeserialize, - deserializeProp: _userDeserializeProp, - idName: r'isarId', - indexes: { - r'id': IndexSchema( - id: -3268401673993471357, - name: r'id', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'id', - type: IndexType.hash, - caseSensitive: true, - ), - ], - ), - }, - links: {}, - embeddedSchemas: {}, - - getId: _userGetId, - getLinks: _userGetLinks, - attach: _userAttach, - version: '3.3.0-dev.3', -); - -int _userEstimateSize( - User object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.email.length * 3; - bytesCount += 3 + object.id.length * 3; - bytesCount += 3 + object.name.length * 3; - bytesCount += 3 + object.profileImagePath.length * 3; - return bytesCount; -} - -void _userSerialize( - User object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByte(offsets[0], object.avatarColor.index); - writer.writeString(offsets[1], object.email); - writer.writeString(offsets[2], object.id); - writer.writeBool(offsets[3], object.inTimeline); - writer.writeBool(offsets[4], object.isAdmin); - writer.writeBool(offsets[5], object.isPartnerSharedBy); - writer.writeBool(offsets[6], object.isPartnerSharedWith); - writer.writeBool(offsets[7], object.memoryEnabled); - writer.writeString(offsets[8], object.name); - writer.writeString(offsets[9], object.profileImagePath); - writer.writeLong(offsets[10], object.quotaSizeInBytes); - writer.writeLong(offsets[11], object.quotaUsageInBytes); - writer.writeDateTime(offsets[12], object.updatedAt); -} - -User _userDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = User( - avatarColor: - _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? - AvatarColor.primary, - email: reader.readString(offsets[1]), - id: reader.readString(offsets[2]), - inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, - isAdmin: reader.readBool(offsets[4]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, - name: reader.readString(offsets[8]), - profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', - quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0, - quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0, - updatedAt: reader.readDateTime(offsets[12]), - ); - return object; -} - -P _userDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ?? - AvatarColor.primary) - as P; - case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readString(offset)) as P; - case 3: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 4: - return (reader.readBool(offset)) as P; - case 5: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 6: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 7: - return (reader.readBoolOrNull(offset) ?? true) as P; - case 8: - return (reader.readString(offset)) as P; - case 9: - return (reader.readStringOrNull(offset) ?? '') as P; - case 10: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 11: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 12: - return (reader.readDateTime(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _UseravatarColorEnumValueMap = { - 'primary': 0, - 'pink': 1, - 'red': 2, - 'yellow': 3, - 'blue': 4, - 'green': 5, - 'purple': 6, - 'orange': 7, - 'gray': 8, - 'amber': 9, -}; -const _UseravatarColorValueEnumMap = { - 0: AvatarColor.primary, - 1: AvatarColor.pink, - 2: AvatarColor.red, - 3: AvatarColor.yellow, - 4: AvatarColor.blue, - 5: AvatarColor.green, - 6: AvatarColor.purple, - 7: AvatarColor.orange, - 8: AvatarColor.gray, - 9: AvatarColor.amber, -}; - -Id _userGetId(User object) { - return object.isarId; -} - -List> _userGetLinks(User object) { - return []; -} - -void _userAttach(IsarCollection col, Id id, User object) {} - -extension UserByIndex on IsarCollection { - Future getById(String id) { - return getByIndex(r'id', [id]); - } - - User? getByIdSync(String id) { - return getByIndexSync(r'id', [id]); - } - - Future deleteById(String id) { - return deleteByIndex(r'id', [id]); - } - - bool deleteByIdSync(String id) { - return deleteByIndexSync(r'id', [id]); - } - - Future> getAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndex(r'id', values); - } - - List getAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return getAllByIndexSync(r'id', values); - } - - Future deleteAllById(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndex(r'id', values); - } - - int deleteAllByIdSync(List idValues) { - final values = idValues.map((e) => [e]).toList(); - return deleteAllByIndexSync(r'id', values); - } - - Future putById(User object) { - return putByIndex(r'id', object); - } - - Id putByIdSync(User object, {bool saveLinks = true}) { - return putByIndexSync(r'id', object, saveLinks: saveLinks); - } - - Future> putAllById(List objects) { - return putAllByIndex(r'id', objects); - } - - List putAllByIdSync(List objects, {bool saveLinks = true}) { - return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); - } -} - -extension UserQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension UserQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between(lower: isarId, upper: isarId), - ); - }); - } - - QueryBuilder isarIdNotEqualTo(Id isarId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), - ); - } - }); - } - - QueryBuilder isarIdGreaterThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id isarId, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.between( - lower: lowerIsarId, - includeLower: includeLower, - upper: upperIsarId, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder idEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IndexWhereClause.equalTo(indexName: r'id', value: [id]), - ); - }); - } - - QueryBuilder idNotEqualTo(String id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ); - } else { - return query - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [id], - includeLower: false, - upper: [], - ), - ) - .addWhereClause( - IndexWhereClause.between( - indexName: r'id', - lower: [], - upper: [id], - includeUpper: false, - ), - ); - } - }); - } -} - -extension UserQueryFilter on QueryBuilder { - QueryBuilder avatarColorEqualTo( - AvatarColor value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'avatarColor', value: value), - ); - }); - } - - QueryBuilder avatarColorGreaterThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorLessThan( - AvatarColor value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'avatarColor', - value: value, - ), - ); - }); - } - - QueryBuilder avatarColorBetween( - AvatarColor lower, - AvatarColor upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'avatarColor', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder emailEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'email', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'email', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'email', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder emailIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'email', value: ''), - ); - }); - } - - QueryBuilder emailIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'email', value: ''), - ); - }); - } - - QueryBuilder idEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'id', value: ''), - ); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'id', value: ''), - ); - }); - } - - QueryBuilder inTimelineEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'inTimeline', value: value), - ); - }); - } - - QueryBuilder isAdminEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isAdmin', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedByEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedBy', value: value), - ); - }); - } - - QueryBuilder isPartnerSharedWithEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isPartnerSharedWith', value: value), - ); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'isarId', value: value), - ); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - ), - ); - }); - } - - QueryBuilder isarIdBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'isarId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder memoryEnabledEqualTo( - bool value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'memoryEnabled', value: value), - ); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'name', value: ''), - ); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'name', value: ''), - ); - }); - } - - QueryBuilder profileImagePathEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'profileImagePath', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.startsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.endsWith( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathContains( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.contains( - property: r'profileImagePath', - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathMatches( - String pattern, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.matches( - property: r'profileImagePath', - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder profileImagePathIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder profileImagePathIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan(property: r'profileImagePath', value: ''), - ); - }); - } - - QueryBuilder quotaSizeInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaSizeInBytes', value: value), - ); - }); - } - - QueryBuilder quotaSizeInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaSizeInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaSizeInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaSizeInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'quotaUsageInBytes', value: value), - ); - }); - } - - QueryBuilder quotaUsageInBytesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'quotaUsageInBytes', - value: value, - ), - ); - }); - } - - QueryBuilder quotaUsageInBytesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'quotaUsageInBytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } - - QueryBuilder updatedAtEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.equalTo(property: r'updatedAt', value: value), - ); - }); - } - - QueryBuilder updatedAtGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.greaterThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.lessThan( - include: include, - property: r'updatedAt', - value: value, - ), - ); - }); - } - - QueryBuilder updatedAtBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - FilterCondition.between( - property: r'updatedAt', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - ), - ); - }); - } -} - -extension UserQueryObject on QueryBuilder {} - -extension UserQueryLinks on QueryBuilder {} - -extension UserQuerySortBy on QueryBuilder { - QueryBuilder sortByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder sortByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder sortByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder sortByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder sortByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder sortByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder sortByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder sortByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder sortByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder sortByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder sortByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder sortByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder sortByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder sortByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder sortByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder sortByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder sortByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder sortByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder sortByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQuerySortThenBy on QueryBuilder { - QueryBuilder thenByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.asc); - }); - } - - QueryBuilder thenByAvatarColorDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'avatarColor', Sort.desc); - }); - } - - QueryBuilder thenByEmail() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.asc); - }); - } - - QueryBuilder thenByEmailDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'email', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.asc); - }); - } - - QueryBuilder thenByInTimelineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'inTimeline', Sort.desc); - }); - } - - QueryBuilder thenByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.asc); - }); - } - - QueryBuilder thenByIsAdminDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isAdmin', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedByDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedBy', Sort.desc); - }); - } - - QueryBuilder thenByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.asc); - }); - } - - QueryBuilder thenByIsPartnerSharedWithDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isPartnerSharedWith', Sort.desc); - }); - } - - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - - QueryBuilder thenByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.asc); - }); - } - - QueryBuilder thenByMemoryEnabledDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'memoryEnabled', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } - - QueryBuilder thenByProfileImagePath() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.asc); - }); - } - - QueryBuilder thenByProfileImagePathDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'profileImagePath', Sort.desc); - }); - } - - QueryBuilder thenByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaSizeInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaSizeInBytes', Sort.desc); - }); - } - - QueryBuilder thenByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.asc); - }); - } - - QueryBuilder thenByQuotaUsageInBytesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'quotaUsageInBytes', Sort.desc); - }); - } - - QueryBuilder thenByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.asc); - }); - } - - QueryBuilder thenByUpdatedAtDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updatedAt', Sort.desc); - }); - } -} - -extension UserQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByAvatarColor() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'avatarColor'); - }); - } - - QueryBuilder distinctByEmail({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'email', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctById({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByInTimeline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'inTimeline'); - }); - } - - QueryBuilder distinctByIsAdmin() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isAdmin'); - }); - } - - QueryBuilder distinctByIsPartnerSharedBy() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedBy'); - }); - } - - QueryBuilder distinctByIsPartnerSharedWith() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isPartnerSharedWith'); - }); - } - - QueryBuilder distinctByMemoryEnabled() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'memoryEnabled'); - }); - } - - QueryBuilder distinctByName({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByProfileImagePath({ - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy( - r'profileImagePath', - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder distinctByQuotaSizeInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaSizeInBytes'); - }); - } - - QueryBuilder distinctByQuotaUsageInBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'quotaUsageInBytes'); - }); - } - - QueryBuilder distinctByUpdatedAt() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updatedAt'); - }); - } -} - -extension UserQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); - }); - } - - QueryBuilder avatarColorProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'avatarColor'); - }); - } - - QueryBuilder emailProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'email'); - }); - } - - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder inTimelineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'inTimeline'); - }); - } - - QueryBuilder isAdminProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isAdmin'); - }); - } - - QueryBuilder isPartnerSharedByProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedBy'); - }); - } - - QueryBuilder isPartnerSharedWithProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isPartnerSharedWith'); - }); - } - - QueryBuilder memoryEnabledProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'memoryEnabled'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } - - QueryBuilder profileImagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'profileImagePath'); - }); - } - - QueryBuilder quotaSizeInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaSizeInBytes'); - }); - } - - QueryBuilder quotaUsageInBytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'quotaUsageInBytes'); - }); - } - - QueryBuilder updatedAtProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updatedAt'); - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index d41891e2ea..eabefbb806 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -1,9 +1,10 @@ +// ignore_for_file: experimental_member_use + import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'; @@ -27,22 +28,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; -import 'package:isar/isar.dart' hide Index; - -// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone -// ref: isar/isar_common.dart -const Symbol _kzoneTxn = #zoneTxn; - -class IsarDatabaseRepository implements IDatabaseRepository { - final Isar _db; - const IsarDatabaseRepository(Isar db) : _db = db; - - // Isar do not support nested transactions. This is a workaround to prevent us from making nested transactions - // Reuse the current transaction if it is already active, else start a new transaction - @override - Future transaction(Future Function() callback) => - Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); -} @DriftDatabase( tables: [ @@ -70,7 +55,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) -class Drift extends $Drift implements IDatabaseRepository { +class Drift extends $Drift { Drift([QueryExecutor? executor]) : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); @@ -261,10 +246,9 @@ class Drift extends $Drift implements IDatabaseRepository { ); } -class DriftDatabaseRepository implements IDatabaseRepository { +class DriftDatabaseRepository { final Drift _db; const DriftDatabaseRepository(this._db); - @override Future transaction(Future Function() callback) => _db.transaction(callback); } diff --git a/mobile/lib/infrastructure/repositories/device_asset.repository.dart b/mobile/lib/infrastructure/repositories/device_asset.repository.dart deleted file mode 100644 index 73ee148ab3..0000000000 --- a/mobile/lib/infrastructure/repositories/device_asset.repository.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarDeviceAssetRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarDeviceAssetRepository(this._db) : super(_db); - - Future deleteIds(List ids) { - return transaction(() async { - await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList()); - }); - } - - Future> getByIds(List localIds) { - return _db.deviceAssetEntitys - .where() - .anyOf(localIds, (query, id) => query.assetIdEqualTo(id)) - .findAll() - .then((value) => value.map((e) => e.toModel()).toList()); - } - - Future updateAll(List assetHash) { - return transaction(() async { - await _db.deviceAssetEntitys.putAll(assetHash.map(DeviceAssetEntity.fromDto).toList()); - return true; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/exif.repository.dart b/mobile/lib/infrastructure/repositories/exif.repository.dart deleted file mode 100644 index 0ede30680e..0000000000 --- a/mobile/lib/infrastructure/repositories/exif.repository.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarExifRepository extends IsarDatabaseRepository { - final Isar _db; - - const IsarExifRepository(this._db) : super(_db); - - Future delete(int assetId) async { - await transaction(() async { - await _db.exifInfos.delete(assetId); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.exifInfos.clear(); - }); - } - - Future get(int assetId) async { - return (await _db.exifInfos.get(assetId))?.toDto(); - } - - Future update(ExifInfo exifInfo) { - return transaction(() async { - await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo)); - return exifInfo; - }); - } - - Future> updateAll(List exifInfos) { - return transaction(() async { - await _db.exifInfos.putAll(exifInfos.map(entity.ExifInfo.fromDto).toList()); - return exifInfos; - }); - } -} diff --git a/mobile/lib/infrastructure/repositories/logger_db.repository.dart b/mobile/lib/infrastructure/repositories/logger_db.repository.dart index e494782fa6..d11174356d 100644 --- a/mobile/lib/infrastructure/repositories/logger_db.repository.dart +++ b/mobile/lib/infrastructure/repositories/logger_db.repository.dart @@ -1,11 +1,10 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; -import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.drift.dart'; @DriftDatabase(tables: [LogMessageEntity]) -class DriftLogger extends $DriftLogger implements IDatabaseRepository { +class DriftLogger extends $DriftLogger { DriftLogger([QueryExecutor? executor]) : super( executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)), diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index df4172df99..6d19d17931 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; +import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; @@ -264,4 +266,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository { Future getCount() { return _db.managers.remoteAssetEntity.count(); } + + Future> getAssetEdits(String assetId) { + final query = _db.assetEditEntity.select() + ..where((row) => row.assetId.equals(assetId) & row.action.equals(AssetEditAction.other.index).not()) + ..orderBy([(row) => OrderingTerm.asc(row.sequence)]); + return query.map((row) => row.toDto()!).get(); + } } diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index d4e34a02f5..9680aa0425 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -1,150 +1,42 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:isar/isar.dart'; -// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite -abstract class IStoreRepository { - Future deleteAll(); - Stream>> watchAll(); - Future delete(StoreKey key); - Future upsert(StoreKey key, T value); - Future tryGet(StoreKey key); - Stream watch(StoreKey key); - Future>> getAll(); -} - -class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository { - final Isar _db; - final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); - - IsarStoreRepository(super.db) : _db = db; - - @override - Future deleteAll() async { - return await transaction(() async { - await _db.storeValues.clear(); - return true; - }); - } - - @override - Stream>> watchAll() { - return _db.storeValues - .filter() - .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) - .watch(fireImmediately: true) - .asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity)))); - } - - @override - Future delete(StoreKey key) async { - return await transaction(() async => await _db.storeValues.delete(key.id)); - } - - @override - Future upsert(StoreKey key, T value) async { - return await transaction(() async { - await _db.storeValues.put(await _fromValue(key, value)); - return true; - }); - } - - @override - Future tryGet(StoreKey key) async { - final entity = (await _db.storeValues.get(key.id)); - if (entity == null) { - return null; - } - return await _toValue(key, entity); - } - - @override - Stream watch(StoreKey key) async* { - yield* _db.storeValues - .watchObject(key.id, fireImmediately: true) - .asyncMap((e) async => e == null ? null : await _toValue(key, e)); - } - - Future> _toUpdateEvent(StoreValue entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; - final value = await _toValue(key, entity); - return StoreDto(key, value); - } - - Future _toValue(StoreKey key, StoreValue entity) async => - switch (key.type) { - const (int) => entity.intValue, - const (String) => entity.strValue, - const (bool) => entity.intValue == 1, - const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), - const (UserDto) => - entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!), - _ => null, - } - as T?; - - Future _fromValue(StoreKey key, T value) async { - final (int? intValue, String? strValue) = switch (key.type) { - const (int) => (value as int, null), - const (String) => (null, value as String), - const (bool) => ((value as bool) ? 1 : 0, null), - const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), - const (UserDto) => (null, (await IsarUserRepository(_db).update(value as UserDto)).id), - _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), - }; - return StoreValue(key.id, intValue: intValue, strValue: strValue); - } - - @override - Future>> getAll() async { - final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll(); - return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); - } -} - -class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository { +class DriftStoreRepository extends DriftDatabaseRepository { final Drift _db; final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); DriftStoreRepository(super.db) : _db = db; - @override Future deleteAll() async { await _db.storeEntity.deleteAll(); return true; } - @override Future>> getAll() async { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); } - @override Stream>> watchAll() { final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); } - @override Future delete(StoreKey key) async { await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); return; } - @override Future upsert(StoreKey key, T value) async { await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); return true; } - @override Future tryGet(StoreKey key) async { final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull(); if (entity == null) { @@ -153,7 +45,6 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo return await _toValue(key, entity); } - @override Stream watch(StoreKey key) async* { final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id)); diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index d4eb1ceed6..ce7cb124db 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,72 +1,9 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; -import 'package:isar/isar.dart'; - -class IsarUserRepository extends IsarDatabaseRepository { - final Isar _db; - const IsarUserRepository(super.db) : _db = db; - - Future delete(List ids) async { - await transaction(() async { - await _db.users.deleteAllById(ids); - }); - } - - Future deleteAll() async { - await transaction(() async { - await _db.users.clear(); - }); - } - - Future> getAll({SortUserBy? sortBy}) async { - return (await _db.users - .where() - .optional( - sortBy != null, - (query) => switch (sortBy!) { - SortUserBy.id => query.sortById(), - }, - ) - .findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Future getByUserId(String id) async { - return (await _db.users.getById(id))?.toDto(); - } - - Future> getByUserIds(List ids) async { - return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList(); - } - - Future insert(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return true; - } - - Future update(UserDto user) async { - await transaction(() async { - await _db.users.put(entity.User.fromDto(user)); - }); - return user; - } - - Future updateAll(List users) async { - await transaction(() async { - await _db.users.putAll(users.map(entity.User.fromDto).toList()); - }); - return true; - } -} class DriftAuthUserRepository extends DriftDatabaseRepository { final Drift _db; @@ -117,6 +54,7 @@ extension on AuthUserEntityData { id: id, email: email, name: name, + updatedAt: profileChangedAt, profileChangedAt: profileChangedAt, hasProfileImage: hasProfileImage, avatarColor: avatarColor, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7e7c709eeb..4a284b9bda 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; @@ -24,7 +23,6 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; @@ -32,9 +30,7 @@ import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -53,23 +49,13 @@ void main() async { ImmichWidgetsBinding(); unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); await EasyLocalization.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); + final (drift, _) = await Bootstrap.initDomain(); await initApp(); // Warm-up isolate pool for worker manager await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); - await migrateDatabaseIfNeeded(isar, drift); + await migrateDatabaseIfNeeded(); - runApp( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const MainWidget(), - ), - ); + runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget())); } catch (error, stack) { runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString())); } @@ -176,7 +162,6 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); - await ref.read(localNotificationService).setup(); } Future _deepLinkBuilder(PlatformDeepLink deepLink) async { @@ -215,20 +200,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - if (Store.isBetaTimelineEnabled) { - ref.read(backgroundServiceProvider).disableService(); - ref.read(backgroundWorkerFgServiceProvider).enable(); - if (Platform.isAndroid) { - ref - .read(backgroundWorkerFgServiceProvider) - .saveNotificationMessage( - StaticTranslations.instance.uploading_media, - StaticTranslations.instance.backup_background_service_default_notification, - ); - } - } else { - ref.read(backgroundWorkerFgServiceProvider).disable(); - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + ref.read(backgroundWorkerFgServiceProvider).enable(); + if (Platform.isAndroid) { + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, + ); } }); diff --git a/mobile/lib/models/albums/album_add_asset_response.model.dart b/mobile/lib/models/albums/album_add_asset_response.model.dart deleted file mode 100644 index 38dd989af5..0000000000 --- a/mobile/lib/models/albums/album_add_asset_response.model.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:collection/collection.dart'; - -class AlbumAddAssetsResponse { - List alreadyInAlbum; - int successfullyAdded; - - AlbumAddAssetsResponse({required this.alreadyInAlbum, required this.successfullyAdded}); - - AlbumAddAssetsResponse copyWith({List? alreadyInAlbum, int? successfullyAdded}) { - return AlbumAddAssetsResponse( - alreadyInAlbum: alreadyInAlbum ?? this.alreadyInAlbum, - successfullyAdded: successfullyAdded ?? this.successfullyAdded, - ); - } - - Map toMap() { - return {'alreadyInAlbum': alreadyInAlbum, 'successfullyAdded': successfullyAdded}; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() => 'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)'; - - @override - bool operator ==(covariant AlbumAddAssetsResponse other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.alreadyInAlbum, alreadyInAlbum) && other.successfullyAdded == successfullyAdded; - } - - @override - int get hashCode => alreadyInAlbum.hashCode ^ successfullyAdded.hashCode; -} diff --git a/mobile/lib/models/albums/album_viewer_page_state.model.dart b/mobile/lib/models/albums/album_viewer_page_state.model.dart deleted file mode 100644 index 70427899ae..0000000000 --- a/mobile/lib/models/albums/album_viewer_page_state.model.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:convert'; - -class AlbumViewerPageState { - final bool isEditAlbum; - final String editTitleText; - final String editDescriptionText; - - const AlbumViewerPageState({ - required this.isEditAlbum, - required this.editTitleText, - required this.editDescriptionText, - }); - - AlbumViewerPageState copyWith({bool? isEditAlbum, String? editTitleText, String? editDescriptionText}) { - return AlbumViewerPageState( - isEditAlbum: isEditAlbum ?? this.isEditAlbum, - editTitleText: editTitleText ?? this.editTitleText, - editDescriptionText: editDescriptionText ?? this.editDescriptionText, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'isEditAlbum': isEditAlbum}); - result.addAll({'editTitleText': editTitleText}); - result.addAll({'editDescriptionText': editDescriptionText}); - - return result; - } - - factory AlbumViewerPageState.fromMap(Map map) { - return AlbumViewerPageState( - isEditAlbum: map['isEditAlbum'] ?? false, - editTitleText: map['editTitleText'] ?? '', - editDescriptionText: map['editDescriptionText'] ?? '', - ); - } - - String toJson() => json.encode(toMap()); - - factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source)); - - @override - String toString() => - 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AlbumViewerPageState && - other.isEditAlbum == isEditAlbum && - other.editTitleText == editTitleText && - other.editDescriptionText == editDescriptionText; - } - - @override - int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode ^ editDescriptionText.hashCode; -} diff --git a/mobile/lib/models/albums/asset_selection_page_result.model.dart b/mobile/lib/models/albums/asset_selection_page_result.model.dart deleted file mode 100644 index cc750f397f..0000000000 --- a/mobile/lib/models/albums/asset_selection_page_result.model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionPageResult { - final Set selectedAssets; - - const AssetSelectionPageResult({required this.selectedAssets}); - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final setEquals = const DeepCollectionEquality().equals; - - return other is AssetSelectionPageResult && setEquals(other.selectedAssets, selectedAssets); - } - - @override - int get hashCode => selectedAssets.hashCode; -} diff --git a/mobile/lib/models/asset_selection_state.dart b/mobile/lib/models/asset_selection_state.dart deleted file mode 100644 index aded3064ce..0000000000 --- a/mobile/lib/models/asset_selection_state.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AssetSelectionState { - final bool hasRemote; - final bool hasLocal; - final bool hasMerged; - final int selectedCount; - - const AssetSelectionState({ - this.hasRemote = false, - this.hasLocal = false, - this.hasMerged = false, - this.selectedCount = 0, - }); - - AssetSelectionState copyWith({bool? hasRemote, bool? hasLocal, bool? hasMerged, int? selectedCount}) { - return AssetSelectionState( - hasRemote: hasRemote ?? this.hasRemote, - hasLocal: hasLocal ?? this.hasLocal, - hasMerged: hasMerged ?? this.hasMerged, - selectedCount: selectedCount ?? this.selectedCount, - ); - } - - AssetSelectionState.fromSelection(Set selection) - : hasLocal = selection.any((e) => e.storage == AssetState.local), - hasMerged = selection.any((e) => e.storage == AssetState.merged), - hasRemote = selection.any((e) => e.storage == AssetState.remote), - selectedCount = selection.length; - - @override - String toString() => - 'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)'; - - @override - bool operator ==(covariant AssetSelectionState other) { - if (identical(this, other)) return true; - - return other.hasRemote == hasRemote && - other.hasLocal == hasLocal && - other.hasMerged == hasMerged && - other.selectedCount == selectedCount; - } - - @override - int get hashCode => hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode ^ selectedCount.hashCode; -} diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart deleted file mode 100644 index 502d0b66be..0000000000 --- a/mobile/lib/models/backup/available_album.model.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; - -class AvailableAlbum { - final Album album; - final int assetCount; - final DateTime? lastBackup; - const AvailableAlbum({required this.album, required this.assetCount, this.lastBackup}); - - AvailableAlbum copyWith({Album? album, int? assetCount, DateTime? lastBackup}) { - return AvailableAlbum( - album: album ?? this.album, - assetCount: assetCount ?? this.assetCount, - lastBackup: lastBackup ?? this.lastBackup, - ); - } - - String get name => album.name; - - String get id => album.localId!; - - bool get isAll => album.isAll; - - @override - String toString() => 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AvailableAlbum && other.album == album; - } - - @override - int get hashCode => album.hashCode; -} diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart deleted file mode 100644 index 01c257dc05..0000000000 --- a/mobile/lib/models/backup/backup_candidate.model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class BackupCandidate { - BackupCandidate({required this.asset, required this.albumNames}); - - Asset asset; - List albumNames; - - @override - int get hashCode => asset.hashCode; - - @override - bool operator ==(Object other) { - if (other is! BackupCandidate) { - return false; - } - return asset == other.asset; - } -} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart deleted file mode 100644 index 51a17de4fc..0000000000 --- a/mobile/lib/models/backup/backup_state.model.dart +++ /dev/null @@ -1,173 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; - -enum BackUpProgressEnum { idle, inProgress, manualInProgress, inBackground, done } - -class BackUpState { - // enum - final BackUpProgressEnum backupProgress; - final List allAssetsInDatabase; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - final double iCloudDownloadProgress; - final ServerDiskInfo serverInfo; - final bool autoBackup; - final bool backgroundBackup; - final bool backupRequireWifi; - final bool backupRequireCharging; - final int backupTriggerDelay; - - /// All available albums on the device - final List availableAlbums; - final Set selectedBackupAlbums; - final Set excludedBackupAlbums; - - /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; - - /// All assets from the selected albums that have been backup - final Set selectedAlbumsBackupAssetsIds; - - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - - const BackUpState({ - required this.backupProgress, - required this.allAssetsInDatabase, - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.iCloudDownloadProgress, - required this.serverInfo, - required this.autoBackup, - required this.backgroundBackup, - required this.backupRequireWifi, - required this.backupRequireCharging, - required this.backupTriggerDelay, - required this.availableAlbums, - required this.selectedBackupAlbums, - required this.excludedBackupAlbums, - required this.allUniqueAssets, - required this.selectedAlbumsBackupAssetsIds, - required this.currentUploadAsset, - }); - - BackUpState copyWith({ - BackUpProgressEnum? backupProgress, - List? allAssetsInDatabase, - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - double? iCloudDownloadProgress, - ServerDiskInfo? serverInfo, - bool? autoBackup, - bool? backgroundBackup, - bool? backupRequireWifi, - bool? backupRequireCharging, - int? backupTriggerDelay, - List? availableAlbums, - Set? selectedBackupAlbums, - Set? excludedBackupAlbums, - Set? allUniqueAssets, - Set? selectedAlbumsBackupAssetsIds, - CurrentUploadAsset? currentUploadAsset, - }) { - return BackUpState( - backupProgress: backupProgress ?? this.backupProgress, - allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase, - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, - serverInfo: serverInfo ?? this.serverInfo, - autoBackup: autoBackup ?? this.autoBackup, - backgroundBackup: backgroundBackup ?? this.backgroundBackup, - backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi, - backupRequireCharging: backupRequireCharging ?? this.backupRequireCharging, - backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay, - availableAlbums: availableAlbums ?? this.availableAlbums, - selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums, - excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums, - allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - ); - } - - @override - String toString() { - return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; - } - - @override - bool operator ==(covariant BackUpState other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other.backupProgress == backupProgress && - collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.iCloudDownloadProgress == iCloudDownloadProgress && - other.serverInfo == serverInfo && - other.autoBackup == autoBackup && - other.backgroundBackup == backgroundBackup && - other.backupRequireWifi == backupRequireWifi && - other.backupRequireCharging == backupRequireCharging && - other.backupTriggerDelay == backupTriggerDelay && - collectionEquals(other.availableAlbums, availableAlbums) && - collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && - collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && - collectionEquals(other.allUniqueAssets, allUniqueAssets) && - collectionEquals(other.selectedAlbumsBackupAssetsIds, selectedAlbumsBackupAssetsIds) && - other.currentUploadAsset == currentUploadAsset; - } - - @override - int get hashCode { - return backupProgress.hashCode ^ - allAssetsInDatabase.hashCode ^ - progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - iCloudDownloadProgress.hashCode ^ - serverInfo.hashCode ^ - autoBackup.hashCode ^ - backgroundBackup.hashCode ^ - backupRequireWifi.hashCode ^ - backupRequireCharging.hashCode ^ - backupTriggerDelay.hashCode ^ - availableAlbums.hashCode ^ - selectedBackupAlbums.hashCode ^ - excludedBackupAlbums.hashCode ^ - allUniqueAssets.hashCode ^ - selectedAlbumsBackupAssetsIds.hashCode ^ - currentUploadAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/current_upload_asset.model.dart b/mobile/lib/models/backup/current_upload_asset.model.dart deleted file mode 100644 index 2214897357..0000000000 --- a/mobile/lib/models/backup/current_upload_asset.model.dart +++ /dev/null @@ -1,95 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -class CurrentUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final int? fileSize; - final bool? iCloudAsset; - - const CurrentUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - this.fileSize, - this.iCloudAsset, - }); - - @pragma('vm:prefer-inline') - bool get isIcloudAsset => iCloudAsset != null && iCloudAsset!; - - CurrentUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - int? fileSize, - bool? iCloudAsset, - }) { - return CurrentUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - fileSize: fileSize ?? this.fileSize, - iCloudAsset: iCloudAsset ?? this.iCloudAsset, - ); - } - - Map toMap() { - return { - 'id': id, - 'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch, - 'fileName': fileName, - 'fileType': fileType, - 'fileSize': fileSize, - 'iCloudAsset': iCloudAsset, - }; - } - - factory CurrentUploadAsset.fromMap(Map map) { - return CurrentUploadAsset( - id: map['id'] as String, - fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), - fileName: map['fileName'] as String, - fileType: map['fileType'] as String, - fileSize: map['fileSize'] as int, - iCloudAsset: map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory CurrentUploadAsset.fromJson(String source) => - CurrentUploadAsset.fromMap(json.decode(source) as Map); - - @override - String toString() { - return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, fileSize: $fileSize, iCloudAsset: $iCloudAsset)'; - } - - @override - bool operator ==(covariant CurrentUploadAsset other) { - if (identical(this, other)) return true; - - return other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.fileSize == fileSize && - other.iCloudAsset == iCloudAsset; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - fileSize.hashCode ^ - iCloudAsset.hashCode; - } -} diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart deleted file mode 100644 index 38f241e748..0000000000 --- a/mobile/lib/models/backup/error_upload_asset.model.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; - -class ErrorUploadAsset { - final String id; - final DateTime fileCreatedAt; - final String fileName; - final String fileType; - final Asset asset; - final String errorMessage; - - const ErrorUploadAsset({ - required this.id, - required this.fileCreatedAt, - required this.fileName, - required this.fileType, - required this.asset, - required this.errorMessage, - }); - - ErrorUploadAsset copyWith({ - String? id, - DateTime? fileCreatedAt, - String? fileName, - String? fileType, - Asset? asset, - String? errorMessage, - }) { - return ErrorUploadAsset( - id: id ?? this.id, - fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, - fileName: fileName ?? this.fileName, - fileType: fileType ?? this.fileType, - asset: asset ?? this.asset, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - String toString() { - return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is ErrorUploadAsset && - other.id == id && - other.fileCreatedAt == fileCreatedAt && - other.fileName == fileName && - other.fileType == fileType && - other.asset == asset && - other.errorMessage == errorMessage; - } - - @override - int get hashCode { - return id.hashCode ^ - fileCreatedAt.hashCode ^ - fileName.hashCode ^ - fileType.hashCode ^ - asset.hashCode ^ - errorMessage.hashCode; - } -} diff --git a/mobile/lib/models/backup/manual_upload_state.model.dart b/mobile/lib/models/backup/manual_upload_state.model.dart deleted file mode 100644 index 120327c611..0000000000 --- a/mobile/lib/models/backup/manual_upload_state.model.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; - -class ManualUploadState { - // Current Backup Asset - final CurrentUploadAsset currentUploadAsset; - final int currentAssetIndex; - - final bool showDetailedNotification; - - /// Manual Upload Stats - final int totalAssetsToUpload; - final int successfulUploads; - final double progressInPercentage; - final String progressInFileSize; - final double progressInFileSpeed; - final List progressInFileSpeeds; - final DateTime progressInFileSpeedUpdateTime; - final int progressInFileSpeedUpdateSentBytes; - - const ManualUploadState({ - required this.progressInPercentage, - required this.progressInFileSize, - required this.progressInFileSpeed, - required this.progressInFileSpeeds, - required this.progressInFileSpeedUpdateTime, - required this.progressInFileSpeedUpdateSentBytes, - required this.currentUploadAsset, - required this.totalAssetsToUpload, - required this.currentAssetIndex, - required this.successfulUploads, - required this.showDetailedNotification, - }); - - ManualUploadState copyWith({ - double? progressInPercentage, - String? progressInFileSize, - double? progressInFileSpeed, - List? progressInFileSpeeds, - DateTime? progressInFileSpeedUpdateTime, - int? progressInFileSpeedUpdateSentBytes, - CurrentUploadAsset? currentUploadAsset, - int? totalAssetsToUpload, - int? successfulUploads, - int? currentAssetIndex, - bool? showDetailedNotification, - }) { - return ManualUploadState( - progressInPercentage: progressInPercentage ?? this.progressInPercentage, - progressInFileSize: progressInFileSize ?? this.progressInFileSize, - progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, - progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, - progressInFileSpeedUpdateTime: progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, - progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? this.progressInFileSpeedUpdateSentBytes, - currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, - totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload, - currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex, - successfulUploads: successfulUploads ?? this.successfulUploads, - showDetailedNotification: showDetailedNotification ?? this.showDetailedNotification, - ); - } - - @override - String toString() { - return 'ManualUploadState(progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final collectionEquals = const DeepCollectionEquality().equals; - - return other is ManualUploadState && - other.progressInPercentage == progressInPercentage && - other.progressInFileSize == progressInFileSize && - other.progressInFileSpeed == progressInFileSpeed && - collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && - other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && - other.progressInFileSpeedUpdateSentBytes == progressInFileSpeedUpdateSentBytes && - other.currentUploadAsset == currentUploadAsset && - other.totalAssetsToUpload == totalAssetsToUpload && - other.currentAssetIndex == currentAssetIndex && - other.successfulUploads == successfulUploads && - other.showDetailedNotification == showDetailedNotification; - } - - @override - int get hashCode { - return progressInPercentage.hashCode ^ - progressInFileSize.hashCode ^ - progressInFileSpeed.hashCode ^ - progressInFileSpeeds.hashCode ^ - progressInFileSpeedUpdateTime.hashCode ^ - progressInFileSpeedUpdateSentBytes.hashCode ^ - currentUploadAsset.hashCode ^ - totalAssetsToUpload.hashCode ^ - currentAssetIndex.hashCode ^ - successfulUploads.hashCode ^ - showDetailedNotification.hashCode; - } -} diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart deleted file mode 100644 index da1e104ba3..0000000000 --- a/mobile/lib/models/backup/success_upload_asset.model.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; - -class SuccessUploadAsset { - final BackupCandidate candidate; - final String remoteAssetId; - final bool isDuplicate; - - const SuccessUploadAsset({required this.candidate, required this.remoteAssetId, required this.isDuplicate}); - - SuccessUploadAsset copyWith({BackupCandidate? candidate, String? remoteAssetId, bool? isDuplicate}) { - return SuccessUploadAsset( - candidate: candidate ?? this.candidate, - remoteAssetId: remoteAssetId ?? this.remoteAssetId, - isDuplicate: isDuplicate ?? this.isDuplicate, - ); - } - - @override - String toString() => - 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; - - @override - bool operator ==(covariant SuccessUploadAsset other) { - if (identical(this, other)) return true; - - return other.candidate == candidate && other.remoteAssetId == remoteAssetId && other.isDuplicate == isDuplicate; - } - - @override - int get hashCode => candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; -} diff --git a/mobile/lib/models/memories/memory.model.dart b/mobile/lib/models/memories/memory.model.dart deleted file mode 100644 index 8a9db5d51b..0000000000 --- a/mobile/lib/models/memories/memory.model.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class Memory { - final String title; - final List assets; - const Memory({required this.title, required this.assets}); - - Memory copyWith({String? title, List? assets}) { - return Memory(title: title ?? this.title, assets: assets ?? this.assets); - } - - @override - String toString() => 'Memory(title: $title, assets: $assets)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is Memory && other.title == title && listEquals(other.assets, assets); - } - - @override - int get hashCode => title.hashCode ^ assets.hashCode; -} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 1b730e0c68..16f3be4655 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -1,8 +1,8 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; class SearchLocationFilter { String? country; diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart deleted file mode 100644 index 02553869bf..0000000000 --- a/mobile/lib/models/search/search_result.model.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResult { - final List assets; - final int? nextPage; - - const SearchResult({required this.assets, this.nextPage}); - - SearchResult copyWith({List? assets, int? nextPage}) { - return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage); - } - - @override - String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)'; - - @override - bool operator ==(covariant SearchResult other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.assets, assets) && other.nextPage == nextPage; - } - - @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; -} diff --git a/mobile/lib/models/search/search_result_page_state.model.dart b/mobile/lib/models/search/search_result_page_state.model.dart deleted file mode 100644 index 7c8a27b50c..0000000000 --- a/mobile/lib/models/search/search_result_page_state.model.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class SearchResultPageState { - final bool isLoading; - final bool isSuccess; - final bool isError; - final bool isSmart; - final List searchResult; - - const SearchResultPageState({ - required this.isLoading, - required this.isSuccess, - required this.isError, - required this.isSmart, - required this.searchResult, - }); - - SearchResultPageState copyWith({ - bool? isLoading, - bool? isSuccess, - bool? isError, - bool? isSmart, - List? searchResult, - }) { - return SearchResultPageState( - isLoading: isLoading ?? this.isLoading, - isSuccess: isSuccess ?? this.isSuccess, - isError: isError ?? this.isError, - isSmart: isSmart ?? this.isSmart, - searchResult: searchResult ?? this.searchResult, - ); - } - - @override - String toString() { - return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isSmart: $isSmart, searchResult: $searchResult)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - final listEquals = const DeepCollectionEquality().equals; - - return other is SearchResultPageState && - other.isLoading == isLoading && - other.isSuccess == isSuccess && - other.isError == isError && - other.isSmart == isSmart && - listEquals(other.searchResult, searchResult); - } - - @override - int get hashCode { - return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ isSmart.hashCode ^ searchResult.hashCode; - } -} diff --git a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart deleted file mode 100644 index f40ac9ccae..0000000000 --- a/mobile/lib/pages/album/album_additional_shared_user_selection.page.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { - final Album album; - - const AlbumAdditionalSharedUserSelectionPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final AsyncValue> suggestedShareUsers = ref.watch(otherUsersProvider); - final sharedUsersList = useState>({}); - - addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text(user.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'suggestions'.tr(), - style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - dense: true, - title: Text(users[index].name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(users[index].email, style: const TextStyle(fontSize: 12)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text('invite_to_album').tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - context.maybePop(null); - }, - ), - actions: [ - TextButton( - onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, - child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - for (var sharedUsers in album.sharedUsers) { - users.removeWhere((u) => u.id == sharedUsers.id || u.id == album.ownerId); - } - - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_asset_selection.page.dart b/mobile/lib/pages/album/album_asset_selection.page.dart deleted file mode 100644 index ccc4c44d43..0000000000 --- a/mobile/lib/pages/album/album_asset_selection.page.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; - -@RoutePage() -class AlbumAssetSelectionPage extends HookConsumerWidget { - const AlbumAssetSelectionPage({super.key, required this.existingAssets, this.canDeselect = false}); - - final Set existingAssets; - final bool canDeselect; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetSelectionRenderList = ref.watch(assetSelectionTimelineProvider); - final selected = useState>(existingAssets); - final selectionEnabledHook = useState(true); - - Widget buildBody(RenderList renderList) { - return ImmichAssetGrid( - renderList: renderList, - listener: (active, assets) { - selectionEnabledHook.value = active; - selected.value = assets; - }, - selectionActive: true, - preselectedAssets: existingAssets, - canDeselect: canDeselect, - showMultiSelectIndicator: false, - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - AutoRouter.of(context).popForced(null); - }, - ), - title: selected.value.isEmpty - ? const Text('add_photos', style: TextStyle(fontSize: 18)).tr() - : const Text( - 'share_assets_selected', - style: TextStyle(fontSize: 18), - ).tr(namedArgs: {'count': selected.value.length.toString()}), - centerTitle: false, - actions: [ - if (selected.value.isNotEmpty || canDeselect) - TextButton( - onPressed: () { - var payload = AssetSelectionPageResult(selectedAssets: selected.value); - AutoRouter.of(context).popForced(payload); - }, - child: Text( - canDeselect ? "done" : "add", - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - ), - ], - ), - body: assetSelectionRenderList.widgetWhen(onData: (data) => buildBody(data)), - ); - } -} diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart deleted file mode 100644 index 578eb839a0..0000000000 --- a/mobile/lib/pages/album/album_control_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; - -class AlbumControlButton extends ConsumerWidget { - final void Function()? onAddPhotosPressed; - final void Function()? onAddUsersPressed; - - const AlbumControlButton({super.key, this.onAddPhotosPressed, this.onAddUsersPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (onAddPhotosPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_photos_button'), - iconData: Icons.add_photo_alternate_outlined, - onPressed: onAddPhotosPressed, - labelText: "add_photos".tr(), - ), - if (onAddUsersPressed != null) - AlbumActionFilledButton( - key: const ValueKey('add_users_button'), - iconData: Icons.person_add_alt_rounded, - onPressed: onAddUsersPressed, - labelText: "album_viewer_page_share_add_users".tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_date_range.dart b/mobile/lib/pages/album/album_date_range.dart deleted file mode 100644 index dbfd9214f1..0000000000 --- a/mobile/lib/pages/album/album_date_range.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; - -class AlbumDateRange extends ConsumerWidget { - const AlbumDateRange({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final data = ref.watch( - currentAlbumProvider.select((album) { - if (album == null || album.assets.isEmpty) { - return null; - } - - final startDate = album.startDate; - final endDate = album.endDate; - if (startDate == null || endDate == null) { - return null; - } - return (startDate, endDate, album.shared); - }), - ); - - if (data == null) { - return const SizedBox(); - } - final (startDate, endDate, shared) = data; - - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - _getDateRangeText(startDate, endDate), - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), - ), - ); - } - - @pragma('vm:prefer-inline') - String _getDateRangeText(DateTime startDate, DateTime endDate) { - if (startDate.day == endDate.day && startDate.month == endDate.month && startDate.year == endDate.year) { - return DateFormat.yMMMd().format(startDate); - } - - final String startDateText = (startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd()).format( - startDate, - ); - final String endDateText = DateFormat.yMMMd().format(endDate); - return "$startDateText - $endDateText"; - } -} diff --git a/mobile/lib/pages/album/album_description.dart b/mobile/lib/pages/album/album_description.dart deleted file mode 100644 index 383367e8b7..0000000000 --- a/mobile/lib/pages/album/album_description.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumDescription extends ConsumerWidget { - const AlbumDescription({super.key, required this.descriptionFocusNode}); - - final FocusNode descriptionFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumDescription) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.description); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableDescription( - albumDescription: albumDescription ?? 'add_a_description'.tr(), - descriptionFocusNode: descriptionFocusNode, - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumDescription ?? 'add_a_description'.tr(), style: context.textTheme.bodyLarge), - ); - } -} diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart deleted file mode 100644 index ca65a92a79..0000000000 --- a/mobile/lib/pages/album/album_options.page.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumOptionsPage extends HookConsumerWidget { - const AlbumOptionsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final sharedUsers = useState(album.sharedUsers.map((u) => u.toDto()).toList()); - final owner = album.owner.value; - final userId = ref.watch(authProvider).userId; - final activityEnabled = useState(album.activityEnabled); - final isProcessing = useProcessingOverlay(); - final isOwner = owner?.id == userId; - - void showErrorMessage() { - context.pop(); - ImmichToast.show( - context: context, - msg: "shared_album_section_people_action_error".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - void leaveAlbum() async { - isProcessing.value = true; - - try { - final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - showErrorMessage(); - } - } catch (_) { - showErrorMessage(); - } - - isProcessing.value = false; - } - - void removeUserFromAlbum(UserDto user) async { - isProcessing.value = true; - - try { - await ref.read(albumProvider.notifier).removeUser(album, user); - album.sharedUsers.remove(entity.User.fromDto(user)); - sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList(); - } catch (error) { - showErrorMessage(); - } - - context.pop(); - isProcessing.value = false; - } - - void handleUserClick(UserDto user) { - var actions = []; - - if (user.id == userId) { - actions = [ - ListTile( - leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("shared_album_section_people_action_leave").tr(), - onTap: leaveAlbum, - ), - ]; - } - - if (isOwner) { - actions = [ - ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text("shared_album_section_people_action_remove_user").tr(), - onTap: () => removeUserFromAlbum(user), - ), - ]; - } - - showModalBottomSheet( - backgroundColor: context.colorScheme.surfaceContainer, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), - ), - ); - }, - ); - } - - buildOwnerInfo() { - return ListTile( - leading: owner != null ? UserCircleAvatar(user: owner.toDto()) : const SizedBox(), - title: Text(album.owner.value?.name ?? "", style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(album.owner.value?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).tr(), - ); - } - - buildSharedUsersList() { - return ListView.builder( - primary: false, - shrinkWrap: true, - itemCount: sharedUsers.value.length, - itemBuilder: (context, index) { - final user = sharedUsers.value[index]; - return ListTile( - leading: UserCircleAvatar(user: user), - title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), - onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null, - ); - }, - ); - } - - buildSectionTitle(String text) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Text(text, style: context.textTheme.bodySmall), - ); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () => context.maybePop(null), - ), - centerTitle: true, - title: Text("options".tr()), - ), - body: ListView( - children: [ - if (isOwner && album.shared) - SwitchListTile.adaptive( - value: activityEnabled.value, - onChanged: (bool value) async { - activityEnabled.value = value; - if (await ref.read(albumProvider.notifier).setActivitystatus(album, value)) { - album.activityEnabled = value; - } - }, - activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, - dense: true, - title: Text( - "comments_and_likes", - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).tr(), - subtitle: Text( - "let_others_respond", - style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - ), - buildSectionTitle("shared_album_section_people_title".tr()), - buildOwnerInfo(), - buildSharedUsersList(), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart deleted file mode 100644 index 7cf6f387ae..0000000000 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class AlbumSharedUserIcons extends HookConsumerWidget { - const AlbumSharedUserIcons({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsers = useRef>(const []); - sharedUsers.value = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const []; - } - - if (album.sharedUsers.length == sharedUsers.value.length) { - return sharedUsers.value; - } - - return album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - }), - ); - - if (sharedUsers.value.isEmpty) { - return const SizedBox(); - } - - return GestureDetector( - onTap: () => context.pushRoute(const AlbumOptionsRoute()), - child: SizedBox( - height: 50, - child: ListView.builder( - padding: const EdgeInsets.only(left: 16, bottom: 8), - scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), - ); - }), - itemCount: sharedUsers.value.length, - ), - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_shared_user_selection.page.dart b/mobile/lib/pages/album/album_shared_user_selection.page.dart deleted file mode 100644 index ec084b1859..0000000000 --- a/mobile/lib/pages/album/album_shared_user_selection.page.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -@RoutePage() -class AlbumSharedUserSelectionPage extends HookConsumerWidget { - const AlbumSharedUserSelectionPage({super.key, required this.assets}); - - final Set assets; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sharedUsersList = useState>({}); - final suggestedShareUsers = ref.watch(otherUsersProvider); - - createSharedAlbum() async { - var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(ref.watch(albumTitleProvider), assets); - - if (newAlbum != null) { - ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - unawaited(context.maybePop(true)); - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } - - ScaffoldMessenger( - child: SnackBar( - content: Text( - 'select_user_for_sharing_page_err_album', - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - ); - } - - buildTileIcon(UserDto user) { - if (sharedUsersList.value.contains(user)) { - return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); - } else { - return UserCircleAvatar(user: user); - } - } - - buildUserList(List users) { - List usersChip = []; - - for (var user in sharedUsersList.value) { - usersChip.add( - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: context.primaryColor.withValues(alpha: 0.15), - label: Text( - user.email, - style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold), - ), - ), - ), - ); - } - return ListView( - children: [ - Wrap(children: [...usersChip]), - Padding( - padding: const EdgeInsets.all(16.0), - child: const Text( - 'suggestions', - style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), - ).tr(), - ), - ListView.builder( - primary: false, - shrinkWrap: true, - itemBuilder: ((context, index) { - return ListTile( - leading: buildTileIcon(users[index]), - title: Text(users[index].email, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - onTap: () { - if (sharedUsersList.value.contains(users[index])) { - sharedUsersList.value = sharedUsersList.value - .where((selectedUser) => selectedUser.id != users[index].id) - .toSet(); - } else { - sharedUsersList.value = {...sharedUsersList.value, users[index]}; - } - }, - ); - }), - itemCount: users.length, - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: Text('invite_to_album', style: TextStyle(color: context.primaryColor)).tr(), - elevation: 0, - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - unawaited(context.maybePop()); - }, - ), - actions: [ - TextButton( - style: TextButton.styleFrom(foregroundColor: context.primaryColor), - onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, - child: const Text( - "create_album", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - // color: context.primaryColor, - ), - ).tr(), - ), - ], - ), - body: suggestedShareUsers.widgetWhen( - onData: (users) { - return buildUserList(users); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/album/album_title.dart b/mobile/lib/pages/album/album_title.dart deleted file mode 100644 index 6c7fc3faaa..0000000000 --- a/mobile/lib/pages/album/album_title.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; - -class AlbumTitle extends ConsumerWidget { - const AlbumTitle({super.key, required this.titleFocusNode}); - - final FocusNode titleFocusNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final (isOwner, isRemote, albumName) = ref.watch( - currentAlbumProvider.select((album) { - if (album == null) { - return const (false, false, ''); - } - - return (album.ownerId == userId, album.isRemote, album.name); - }), - ); - - if (isOwner && isRemote) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: AlbumViewerEditableTitle(albumName: albumName, titleFocusNode: titleFocusNode), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumName, style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700)), - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart deleted file mode 100644 index 97853fb96a..0000000000 --- a/mobile/lib/pages/album/album_viewer.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/pages/album/album_control_button.dart'; -import 'package:immich_mobile/pages/album/album_date_range.dart'; -import 'package:immich_mobile/pages/album/album_description.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_icons.dart'; -import 'package:immich_mobile/pages/album/album_title.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewer extends HookConsumerWidget { - const AlbumViewer({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider); - if (album == null) { - return const SizedBox(); - } - - final titleFocusNode = useFocusNode(); - final descriptionFocusNode = useFocusNode(); - final userId = ref.watch(authProvider).userId; - final isMultiselecting = ref.watch(multiselectProvider); - final isProcessing = useProcessingOverlay(); - final isOwner = ref.watch( - currentAlbumProvider.select((album) { - return album?.ownerId == userId; - }), - ); - - Future onRemoveFromAlbumPressed(Iterable assets) async { - final bool isSuccess = await ref.read(albumProvider.notifier).removeAsset(album, assets); - - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - return isSuccess; - } - - /// Find out if the assets in album exist on the device - /// If they exist, add to selected asset state to show they are already selected. - void onAddPhotosPressed() async { - AssetSelectionPageResult? returnPayload = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: album.assets, canDeselect: false), - ); - - if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { - // Check if there is new assets add - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addAssets(album, returnPayload.selectedAssets); - - isProcessing.value = false; - } - } - - void onAddUsersPressed() async { - List? sharedUserIds = await context.pushRoute?>( - AlbumAdditionalSharedUserSelectionRoute(album: album), - ); - - if (sharedUserIds != null) { - isProcessing.value = true; - - await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds); - - isProcessing.value = false; - } - } - - onActivitiesPressed() { - if (album.remoteId != null) { - ref.read(currentAssetProvider.notifier).set(null); - context.pushRoute(const ActivitiesRoute()); - } - } - - return Stack( - children: [ - MultiselectGrid( - key: const ValueKey("albumViewerMultiselectGrid"), - renderListProvider: albumTimelineProvider(album.id), - topWidget: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - context.primaryColor.withValues(alpha: 0.06), - context.primaryColor.withValues(alpha: 0.04), - Colors.indigo.withValues(alpha: 0.02), - Colors.transparent, - ], - stops: const [0.0, 0.3, 0.7, 1.0], - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 32), - const AlbumDateRange(), - AlbumTitle(key: const ValueKey("albumTitle"), titleFocusNode: titleFocusNode), - AlbumDescription(key: const ValueKey("albumDescription"), descriptionFocusNode: descriptionFocusNode), - const AlbumSharedUserIcons(), - if (album.isRemote) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: isOwner ? onAddUsersPressed : null, - ), - ), - const SizedBox(height: 8), - ], - ), - ), - onRemoveFromAlbum: onRemoveFromAlbumPressed, - editEnabled: album.ownerId == userId, - ), - AnimatedPositioned( - key: const ValueKey("albumViewerAppbarPositioned"), - duration: const Duration(milliseconds: 300), - top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: AlbumViewerAppbar( - key: const ValueKey("albumViewerAppbar"), - titleFocusNode: titleFocusNode, - descriptionFocusNode: descriptionFocusNode, - userId: userId, - onAddPhotos: onAddPhotosPressed, - onAddUsers: onAddUsersPressed, - onActivities: onActivitiesPressed, - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/album/album_viewer.page.dart b/mobile/lib/pages/album/album_viewer.page.dart deleted file mode 100644 index c99dacd9b7..0000000000 --- a/mobile/lib/pages/album/album_viewer.page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/pages/album/album_viewer.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; - -@RoutePage() -class AlbumViewerPage extends HookConsumerWidget { - final int albumId; - - const AlbumViewerPage({super.key, required this.albumId}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page - ref.listen(currentAlbumProvider, (_, __) {}); - - // This call helps rendering the asset selection instantly - ref.listen(assetSelectionTimelineProvider, (_, __) {}); - - ref.listen(albumWatcher(albumId), (_, albumFuture) { - albumFuture.whenData((value) => ref.read(currentAlbumProvider.notifier).set(value)); - }); - - return const Scaffold(body: AlbumViewer()); - } -} diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart deleted file mode 100644 index 5f155c2f0d..0000000000 --- a/mobile/lib/pages/albums/albums.page.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; - -@RoutePage() -class AlbumsPage extends HookConsumerWidget { - const AlbumsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((album) => album.isRemote).toList(); - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); - final isGrid = useState(false); - final searchController = useTextEditingController(); - final debounceTimer = useRef(null); - final filterMode = useState(QuickFilterMode.all); - final userId = ref.watch(currentUserProvider)?.id; - final searchFocusNode = useFocusNode(); - - toggleViewMode() { - isGrid.value = !isGrid.value; - } - - onSearch(String searchTerm, QuickFilterMode mode) { - debounceTimer.value?.cancel(); - debounceTimer.value = Timer(const Duration(milliseconds: 300), () { - ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode); - }); - } - - changeFilter(QuickFilterMode mode) { - filterMode.value = mode; - } - - useEffect(() { - searchController.addListener(() { - onSearch(searchController.text, filterMode.value); - }); - - return () { - searchController.removeListener(() { - onSearch(searchController.text, filterMode.value); - }); - debounceTimer.value?.cancel(); - }; - }, []); - - clearSearch() { - filterMode.value = QuickFilterMode.all; - searchController.clear(); - onSearch('', QuickFilterMode.all); - } - - return Scaffold( - appBar: ImmichAppBar( - showUploadButton: false, - actions: [ - IconButton( - icon: const Icon(Icons.add_rounded, size: 28), - onPressed: () => context.pushRoute(CreateAlbumRoute()), - ), - ], - ), - body: RefreshIndicator( - displacement: 70, - onRefresh: () async { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - }, - child: ListView( - shrinkWrap: true, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), - children: [ - Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - transform: const GradientRotation(0.5 * pi), - ), - ), - child: SearchField( - autofocus: false, - contentPadding: const EdgeInsets.all(16), - hintText: 'search_albums'.tr(), - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: searchController.text.isNotEmpty - ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: clearSearch) - : null, - controller: searchController, - onChanged: (_) => onSearch(searchController.text, filterMode.value), - focusNode: searchFocusNode, - onTapOutside: (_) => searchFocusNode.unfocus(), - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - QuickFilterButton( - label: 'all'.tr(), - isSelected: filterMode.value == QuickFilterMode.all, - onTap: () { - changeFilter(QuickFilterMode.all); - onSearch(searchController.text, QuickFilterMode.all); - }, - ), - QuickFilterButton( - label: 'shared_with_me'.tr(), - isSelected: filterMode.value == QuickFilterMode.sharedWithMe, - onTap: () { - changeFilter(QuickFilterMode.sharedWithMe); - onSearch(searchController.text, QuickFilterMode.sharedWithMe); - }, - ), - QuickFilterButton( - label: 'my_albums'.tr(), - isSelected: filterMode.value == QuickFilterMode.myAlbums, - onTap: () { - changeFilter(QuickFilterMode.myAlbums); - onSearch(searchController.text, QuickFilterMode.myAlbums); - }, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SortButton(), - IconButton( - icon: Icon(isGrid.value ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), - onPressed: toggleViewMode, - ), - ], - ), - const SizedBox(height: 5), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: isGrid.value - ? GridView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - itemBuilder: (context, index) { - return AlbumThumbnailCard( - album: sorted[index], - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - showOwner: true, - ); - }, - itemCount: sorted.length, - ) - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: sorted.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - title: Text( - sorted[index].name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: sorted[index].ownerId != null - ? Text( - '${'items_count'.t(context: context, args: {'count': sorted[index].assetCount})} • ${sorted[index].ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': sorted[index].ownerName!}) : 'owned'.t(context: context)}', - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), - ) - : null, - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: sorted[index].id)), - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: sorted[index].thumbnail.value, width: 80, height: 80), - ), - // minVerticalPadding: 1, - ), - ); - }, - ), - ), - ], - ), - ), - resizeToAvoidBottomInset: false, - ); - } -} - -class QuickFilterButton extends StatelessWidget { - const QuickFilterButton({super.key, required this.isSelected, required this.onTap, required this.label}); - - final bool isSelected; - final VoidCallback onTap; - final String label; - - @override - Widget build(BuildContext context) { - return TextButton( - onPressed: onTap, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(isSelected ? context.colorScheme.primary : Colors.transparent), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1), - ), - ), - ), - child: Text( - label, - style: TextStyle( - color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, - fontSize: 14, - ), - ), - ); - } -} - -class SortButton extends ConsumerWidget { - const SortButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - - return MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - consumeOutsideTap: true, - menuChildren: AlbumSortMode.values - .map( - (mode) => MenuItemButton( - leadingIcon: albumSortOption == mode - ? albumSortIsReverse - ? Icon( - Icons.keyboard_arrow_down, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : Icon( - Icons.keyboard_arrow_up_rounded, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface, - ) - : const Icon(Icons.abc, color: Colors.transparent), - onPressed: () { - final selected = albumSortOption == mode; - // Switch direction - if (selected) { - ref.read(albumSortOrderProvider.notifier).changeSortDirection(!albumSortIsReverse); - } else { - ref.read(albumSortByOptionsProvider.notifier).changeSortMode(mode); - } - }, - style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)), - backgroundColor: WidgetStateProperty.all( - albumSortOption == mode ? context.colorScheme.primary : Colors.transparent, - ), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - ), - child: Text( - mode.label.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: albumSortOption == mode - ? context.colorScheme.onPrimary - : context.colorScheme.onSurface.withAlpha(185), - ), - ), - ), - ) - .toList(), - builder: (context, controller, child) { - return GestureDetector( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5), - child: Transform.rotate( - angle: 90 * pi / 180, - child: Icon( - Icons.compare_arrows_rounded, - size: 18, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ), - Text( - albumSortOption.label.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(225), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart deleted file mode 100644 index def31afcd4..0000000000 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class AlbumPreviewPage extends HookConsumerWidget { - final Album album; - const AlbumPreviewPage({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assets = useState>([]); - - getAssetsInAlbum() async { - assets.value = await ref.read(albumMediaRepositoryProvider).getAssets(album.localId!); - } - - useEffect(() { - getAssetsInAlbum(); - return null; - }, []); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Column( - children: [ - Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - "ID ${album.id}", - style: TextStyle( - fontSize: 10, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_new_rounded)), - ), - body: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - crossAxisSpacing: 2, - mainAxisSpacing: 2, - ), - itemCount: assets.value.length, - itemBuilder: (context, index) { - return ImmichThumbnail(asset: assets.value[index], width: 100, height: 100); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart deleted file mode 100644 index d222211577..0000000000 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/backup/album_info_card.dart'; -import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; - -@RoutePage() -class BackupAlbumSelectionPage extends HookConsumerWidget { - const BackupAlbumSelectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; - final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final enableSyncUploadAlbum = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isDarkTheme = context.isDarkTheme; - final albums = ref.watch(backupProvider).availableAlbums; - - useEffect(() { - ref.watch(backupProvider.notifier).getBackupInfo(); - return null; - }, []); - - buildAlbumSelectionList() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - sliver: SliverList( - delegate: SliverChildBuilderDelegate(((context, index) { - return AlbumInfoListTile(album: albums[index]); - }), childCount: albums.length), - ), - ); - } - - buildAlbumSelectionGrid() { - if (albums.isEmpty) { - return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())); - } - - return SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - ), - itemCount: albums.length, - itemBuilder: ((context, index) { - return AlbumInfoCard(album: albums[index]); - }), - ), - ); - } - - buildSelectedAlbumNameChip() { - return selectedBackupAlbums.map((album) { - void removeSelection() => ref.read(backupProvider.notifier).removeAlbumForBackup(album); - - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: GestureDetector( - onTap: removeSelection, - child: Chip( - label: Text( - album.name, - style: TextStyle( - fontSize: 12, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: context.primaryColor, - deleteIconColor: isDarkTheme ? Colors.black : Colors.white, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - buildExcludedAlbumNameChip() { - return excludedBackupAlbums.map((album) { - void removeSelection() { - ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } - - return GestureDetector( - onTap: removeSelection, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Chip( - label: Text( - album.name, - style: TextStyle(fontSize: 12, color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.red[300], - deleteIconColor: context.scaffoldBackgroundColor, - deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, - ), - ), - ); - }).toSet(); - } - - handleSyncAlbumToggle(bool isEnable) async { - if (isEnable) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - for (final album in selectedBackupAlbums) { - await ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - } - - return Scaffold( - appBar: AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - title: const Text("backup_album_selection_page_select_albums").tr(), - elevation: 0, - ), - body: CustomScrollView( - physics: const ClampingScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text("backup_album_selection_page_selection_info", style: context.textTheme.titleSmall).tr(), - ), - - // Selected Album Chips - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Wrap(children: [...buildSelectedAlbumNameChip(), ...buildExcludedAlbumNameChip()]), - ), - - SettingsSwitchListTile( - valueNotifier: enableSyncUploadAlbum, - title: "sync_albums".tr(), - subtitle: "sync_upload_album_setting_subtitle".tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), - subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), - onChanged: handleSyncAlbumToggle, - ), - - ListTile( - title: Text( - "backup_album_selection_page_albums_device".tr( - namedArgs: {'count': ref.watch(backupProvider).availableAlbums.length.toString()}, - ), - style: context.textTheme.titleSmall, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - "backup_album_selection_page_albums_tap", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ).tr(), - ), - trailing: IconButton( - splashRadius: 16, - icon: Icon(Icons.info, size: 20, color: context.primaryColor), - onPressed: () { - // show the dialog - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - elevation: 5, - title: Text( - 'backup_album_selection_page_selection_info', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), - content: SingleChildScrollView( - child: ListBody( - children: [ - const Text( - 'backup_album_selection_page_assets_scatter', - style: TextStyle(fontSize: 14), - ).tr(), - ], - ), - ), - ); - }, - ); - }, - ), - ), - - // buildSearchBar(), - ], - ), - ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (constraints.crossAxisExtent > 600) { - return buildAlbumSelectionGrid(); - } else { - return buildAlbumSelectionList(); - } - }, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart deleted file mode 100644 index 1e008be1bb..0000000000 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ /dev/null @@ -1,286 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; -import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -@RoutePage() -class BackupControllerPage extends HookConsumerWidget { - const BackupControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - BackUpState backupState = ref.watch(backupProvider); - final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; - final didGetBackupInfo = useState(false); - - bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; - bool shouldBackup = - backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 || - !hasExclusiveAccess - ? false - : true; - - useEffect(() { - // Update the background settings information just to make sure we - // have the latest, since the platform channel will not update - // automatically - if (Platform.isIOS) { - ref.watch(iOSBackgroundSettingsProvider.notifier).refresh(); - } - - ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success'); - - return () { - WakelockPlus.disable(); - }; - }, []); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.idle && !didGetBackupInfo.value) { - ref.watch(backupProvider.notifier).getBackupInfo(); - didGetBackupInfo.value = true; - } - return null; - }, [backupState.backupProgress]); - - useEffect(() { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - WakelockPlus.enable(); - } else { - WakelockPlus.disable(); - } - - return null; - }, [backupState.backupProgress]); - - Widget buildSelectedAlbumName() { - var text = "backup_controller_page_backup_selected".tr(); - var albums = ref.watch(backupProvider).selectedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (${'all'.tr()}), "; - } else { - text += "${album.name}, "; - } - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } else { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - "backup_controller_page_none_selected".tr(), - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ), - ); - } - } - - Widget buildExcludedAlbumName() { - var text = "backup_controller_page_excluded".tr(); - var albums = ref.watch(backupProvider).excludedBackupAlbums; - - if (albums.isNotEmpty) { - for (var album in albums) { - text += "${album.name}, "; - } - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - text.trim().substring(0, text.length - 2), - style: context.textTheme.labelLarge?.copyWith(color: Colors.red[300]), - ), - ); - } else { - return const SizedBox(); - } - } - - buildFolderSelectionTile() { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide(color: context.colorScheme.outlineVariant, width: 1), - ), - elevation: 0, - borderOnForeground: false, - child: ListTile( - minVerticalPadding: 18, - title: Text("backup_controller_page_albums", style: context.textTheme.titleMedium).tr(), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - buildSelectedAlbumName(), - buildExcludedAlbumName(), - ], - ), - ), - trailing: ElevatedButton( - onPressed: () async { - await context.pushRoute(const BackupAlbumSelectionRoute()); - // waited until returning from selection - await ref.read(backupProvider.notifier).backupAlbumSelectionDone(); - // waited until backup albums are stored in DB - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - }, - child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - ), - ); - } - - void startBackup() { - ref.watch(errorBackupListProvider.notifier).empty(); - if (ref.watch(backupProvider).backupProgress != BackUpProgressEnum.inBackground) { - ref.watch(backupProvider.notifier).startBackupProcess(); - } - } - - Widget buildBackupButton() { - return Padding( - padding: const EdgeInsets.only(top: 24), - child: Container( - child: - backupState.backupProgress == BackUpProgressEnum.inProgress || - backupState.backupProgress == BackUpProgressEnum.manualInProgress - ? ElevatedButton( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.grey[50], - backgroundColor: Colors.red[300], - // padding: const EdgeInsets.all(14), - ), - onPressed: () { - if (backupState.backupProgress == BackUpProgressEnum.manualInProgress) { - ref.read(manualUploadProvider.notifier).cancelBackup(); - } else { - ref.read(backupProvider.notifier).cancelBackup(); - } - }, - child: const Text("cancel", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ) - : ElevatedButton( - onPressed: shouldBackup ? startBackup : null, - child: const Text( - "backup_controller_page_start_backup", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ).tr(), - ), - ), - ); - } - - buildBackgroundBackupInfo() { - return ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text('background_backup_running_error'.tr()), - ); - } - - buildLoadingIndicator() { - return const Padding( - padding: EdgeInsets.only(top: 42.0), - child: Center(child: CircularProgressIndicator()), - ); - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_controller_page_backup").tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => context.pushRoute(const BackupOptionsRoute()), - splashRadius: 24, - icon: const Icon(Icons.settings_outlined), - ), - ), - ], - ), - body: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [buildFolderSelectionTile(), if (!didGetBackupInfo.value) buildLoadingIndicator()], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/pages/backup/backup_options.page.dart b/mobile/lib/pages/backup/backup_options.page.dart deleted file mode 100644 index 846a32a742..0000000000 --- a/mobile/lib/pages/backup/backup_options.page.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; - -@RoutePage() -class BackupOptionsPage extends StatelessWidget { - const BackupOptionsPage({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text("backup_options_page_title").tr(), - leading: IconButton( - onPressed: () => context.maybePop(true), - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: const BackupSettings(), - ); - } -} diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart deleted file mode 100644 index a97a133b89..0000000000 --- a/mobile/lib/pages/backup/failed_backup_status.page.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:intl/intl.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -@RoutePage() -class FailedBackupStatusPage extends HookConsumerWidget { - const FailedBackupStatusPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final errorBackupList = ref.watch(errorBackupListProvider); - - return Scaffold( - appBar: AppBar( - elevation: 0, - title: Text( - "Failed Backup (${errorBackupList.length})", - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - leading: IconButton( - onPressed: () { - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: ListView.builder( - shrinkWrap: true, - itemCount: errorBackupList.length, - itemBuilder: ((context, index) { - var errorAsset = errorBackupList.elementAt(index); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4), - child: Card( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(15), // if you need this - ), - side: BorderSide(color: Colors.black12, width: 1), - ), - elevation: 0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100, minHeight: 100, maxWidth: 100, maxHeight: 150), - child: ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(15), - topLeft: Radius.circular(15), - ), - clipBehavior: Clip.hardEdge, - child: Image( - fit: BoxFit.cover, - image: LocalThumbProvider(id: errorAsset.asset.localId!, assetType: base_asset.AssetType.video), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - DateFormat.yMMMMd().format( - DateTime.parse(errorAsset.fileCreatedAt.toString()).toLocal(), - ), - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - Icon(Icons.error, color: Colors.red.withAlpha(200), size: 18), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - errorAsset.fileName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - Text( - errorAsset.errorMessage, - style: TextStyle( - fontWeight: FontWeight.w500, - color: context.isDarkTheme ? Colors.white70 : Colors.grey[800], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - }), - ), - ); - } -} diff --git a/mobile/lib/pages/common/activities.page.dart b/mobile/lib/pages/common/activities.page.dart deleted file mode 100644 index 9d1123dbca..0000000000 --- a/mobile/lib/pages/common/activities.page.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; - -@RoutePage() -class ActivitiesPage extends HookConsumerWidget { - const ActivitiesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Album has to be set in the provider before reaching this page - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final user = ref.watch(currentUserProvider); - - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final activities = ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId)); - - final listViewScrollController = useScrollController(); - - Future onAddComment(String comment) async { - await activityNotifier.addComment(comment); - // Scroll to the end of the list to show the newly added activity - await listViewScrollController.animateTo( - listViewScrollController.position.maxScrollExtent + 200, - duration: const Duration(milliseconds: 600), - curve: Curves.fastOutSlowIn, - ); - } - - return Scaffold( - appBar: AppBar(title: asset == null ? Text(album.name) : null), - body: activities.widgetWhen( - onData: (data) { - final liked = data.firstWhereOrNull( - (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.remoteId, - ); - - return SafeArea( - child: Stack( - children: [ - ListView.builder( - controller: listViewScrollController, - // +1 to display an additional over-scroll space after the last element - itemCount: data.length + 1, - itemBuilder: (context, index) { - // Additional vertical gap after the last element - if (index == data.length) { - return const SizedBox(height: 80); - } - - final activity = data[index]; - final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; - - return Padding( - padding: const EdgeInsets.all(5), - child: DismissibleActivity( - activity.id, - ActivityTile(activity), - onDismiss: canDelete - ? (activityId) async => await activityNotifier.removeActivity(activity.id) - : null, - ), - ); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - color: context.scaffoldBackgroundColor, - child: ActivityTextField( - isEnabled: album.activityEnabled, - likeId: liked?.id, - onSubmit: onAddComment, - ), - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart deleted file mode 100644 index 2cc3dede1e..0000000000 --- a/mobile/lib/pages/common/change_experience.page.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class ChangeExperiencePage extends ConsumerStatefulWidget { - final bool switchingToBeta; - - const ChangeExperiencePage({super.key, required this.switchingToBeta}); - - @override - ConsumerState createState() => _ChangeExperiencePageState(); -} - -class _ChangeExperiencePageState extends ConsumerState { - AsyncValue hasMigrated = const AsyncValue.loading(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration()); - } - - Future _handleMigration() async { - try { - await _performMigrationLogic().timeout( - const Duration(minutes: 3), - onTimeout: () async { - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - }, - ); - - if (mounted) { - setState(() { - HapticFeedback.heavyImpact(); - hasMigrated = const AsyncValue.data(true); - }); - } - } catch (e, s) { - Logger("ChangeExperiencePage").severe("Error during migration", e, s); - if (mounted) { - setState(() { - hasMigrated = AsyncValue.error(e, s); - }); - } - } - } - - Future _performMigrationLogic() async { - if (widget.switchingToBeta) { - final assetNotifier = ref.read(assetProvider.notifier); - if (assetNotifier.mounted) { - assetNotifier.dispose(); - } - final albumNotifier = ref.read(albumProvider.notifier); - if (albumNotifier.mounted) { - albumNotifier.dispose(); - } - - // Cancel uploads - await Store.put(StoreKey.backgroundBackup, false); - ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {}); - ref.read(backupProvider.notifier).setAutoBackup(false); - ref.read(backupProvider.notifier).cancelBackup(); - ref.read(manualUploadProvider.notifier).cancelBackup(); - // Start listening to new websocket events - ref.read(websocketProvider.notifier).stopListenToOldEvents(); - ref.read(websocketProvider.notifier).startListeningToBetaEvents(); - - await ref.read(driftProvider).reset(); - await Store.put(StoreKey.shouldResetSync, true); - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - - if (permission.isGranted) { - await ref.read(backgroundSyncProvider).syncLocal(full: true); - await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).disableService(); - } - } else { - await ref.read(backgroundSyncProvider).cancel(); - ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); - ref.read(websocketProvider.notifier).startListeningToOldEvents(); - ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); - await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); - await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - await ref.read(backgroundWorkerFgServiceProvider).disable(); - } - - await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0), - error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0), - loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()), - ), - ), - const SizedBox(height: 16.0), - SizedBox( - width: 300.0, - child: AnimatedSwitcher( - duration: Durations.long4, - child: hasMigrated.when( - data: (data) => Text( - "Migration success!\nPlease close and reopen the app to apply changes", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - error: (error, stackTrace) => Text( - "Migration failed!\nError: $error", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - loading: () => Text( - "Data migration in progress...\nPlease wait and don't close this page", - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart deleted file mode 100644 index 0a28dfeb5a..0000000000 --- a/mobile/lib/pages/common/create_album.page.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; -import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; -import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; -import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; - -@RoutePage() -// ignore: must_be_immutable -class CreateAlbumPage extends HookConsumerWidget { - final List? assets; - - const CreateAlbumPage({super.key, this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty); - final albumTitleTextFieldFocusNode = useFocusNode(); - final albumDescriptionTextFieldFocusNode = useFocusNode(); - final isAlbumTitleTextFieldFocus = useState(false); - final isAlbumTitleEmpty = useState(true); - final selectedAssets = useState>(assets != null ? Set.from(assets!) : const {}); - - void onBackgroundTapped() { - albumTitleTextFieldFocusNode.unfocus(); - albumDescriptionTextFieldFocusNode.unfocus(); - isAlbumTitleTextFieldFocus.value = false; - - if (albumTitleController.text.isEmpty) { - albumTitleController.text = 'create_album_page_untitled'.tr(); - isAlbumTitleEmpty.value = false; - ref.watch(albumTitleProvider.notifier).setAlbumTitle('create_album_page_untitled'.tr()); - } - } - - onSelectPhotosButtonPressed() async { - AssetSelectionPageResult? selectedAsset = await context.pushRoute( - AlbumAssetSelectionRoute(existingAssets: selectedAssets.value, canDeselect: true), - ); - if (selectedAsset == null) { - selectedAssets.value = const {}; - } else { - selectedAssets.value = selectedAsset.selectedAssets; - } - } - - buildTitleInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumTitleTextField( - isAlbumTitleEmpty: isAlbumTitleEmpty, - albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, - albumTitleController: albumTitleController, - isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus, - ), - ); - } - - buildDescriptionInputField() { - return Padding( - padding: const EdgeInsets.only(right: 10, left: 10), - child: AlbumViewerEditableDescription( - albumDescription: '', - descriptionFocusNode: albumDescriptionTextFieldFocusNode, - ), - ); - } - - buildTitle() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 200, left: 18), - child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).tr(), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildSelectPhotosButton() { - if (selectedAssets.value.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 16, left: 16, right: 16), - child: FilledButton.icon( - style: FilledButton.styleFrom( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - backgroundColor: context.colorScheme.surfaceContainerHigh, - ), - onPressed: onSelectPhotosButtonPressed, - icon: Icon(Icons.add_rounded, color: context.primaryColor), - label: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'create_shared_album_page_share_select_photos', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - ), - ), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - buildControlButton() { - return Padding( - padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), - child: SizedBox( - height: 42, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - AlbumActionFilledButton( - iconData: Icons.add_photo_alternate_outlined, - onPressed: onSelectPhotosButtonPressed, - labelText: "add_photos".tr(), - ), - ], - ), - ), - ); - } - - buildSelectedImageGrid() { - if (selectedAssets.value.isNotEmpty) { - return SliverPadding( - padding: const EdgeInsets.only(top: 16), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 5.0, - mainAxisSpacing: 5, - ), - delegate: SliverChildBuilderDelegate((BuildContext context, int index) { - return GestureDetector( - onTap: onBackgroundTapped, - child: SharedAlbumThumbnailImage(asset: selectedAssets.value.elementAt(index)), - ); - }, childCount: selectedAssets.value.length), - ), - ); - } - - return const SliverToBoxAdapter(); - } - - Future createAlbum() async { - onBackgroundTapped(); - var newAlbum = await ref - .watch(albumProvider.notifier) - .createAlbum(ref.read(albumTitleProvider), selectedAssets.value); - - if (newAlbum != null) { - await ref.read(albumProvider.notifier).refreshRemoteAlbums(); - selectedAssets.value = {}; - ref.read(albumTitleProvider.notifier).clearAlbumTitle(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - unawaited(context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id))); - } - } - - return Scaffold( - appBar: AppBar( - elevation: 0, - centerTitle: false, - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - onPressed: () { - selectedAssets.value = {}; - context.maybePop(); - }, - icon: const Icon(Icons.close_rounded), - ), - title: const Text('create_album').tr(), - actions: [ - TextButton( - onPressed: albumTitleController.text.isNotEmpty ? createAlbum : null, - child: Text( - 'create'.tr(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: albumTitleController.text.isNotEmpty ? context.primaryColor : context.themeData.disabledColor, - ), - ), - ), - ], - ), - body: GestureDetector( - onTap: onBackgroundTapped, - child: CustomScrollView( - slivers: [ - SliverAppBar( - backgroundColor: context.scaffoldBackgroundColor, - elevation: 5, - automaticallyImplyLeading: false, - pinned: true, - floating: false, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(125.0), - child: Column( - children: [ - buildTitleInputField(), - buildDescriptionInputField(), - if (selectedAssets.value.isNotEmpty) buildControlButton(), - ], - ), - ), - ), - buildTitle(), - buildSelectPhotosButton(), - buildSelectedImageGrid(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart deleted file mode 100644 index 68123509ae..0000000000 --- a/mobile/lib/pages/common/gallery_stacked_children.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; - -class GalleryStackedChildren extends HookConsumerWidget { - final ValueNotifier stackIndex; - - const GalleryStackedChildren(this.stackIndex, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - - final stackId = asset.stackId; - if (stackId == null) { - return const SizedBox(); - } - - final stackElements = ref.watch(assetStackStateProvider(stackId)); - final showControls = ref.watch(showControlsProvider); - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: SizedBox( - height: 80, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30), - itemBuilder: (context, index) { - final currentAsset = stackElements.elementAt(index); - final assetId = currentAsset.remoteId; - if (assetId == null) { - return const SizedBox(); - } - - return Padding( - key: ValueKey(currentAsset.id), - padding: const EdgeInsets.only(right: 5), - child: GestureDetector( - onTap: () { - stackIndex.value = index; - ref.read(currentAssetProvider.notifier).set(currentAsset); - }, - child: Container( - width: 60, - height: 60, - decoration: index == stackIndex.value - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)), - ) - : const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(6)), - border: null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Image( - fit: BoxFit.cover, - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: asset.thumbhash ?? ""), - ), - ), - ), - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart deleted file mode 100644 index 1d43bff167..0000000000 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ /dev/null @@ -1,438 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/pages/common/download_panel.dart'; -import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart'; -import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart'; -import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart'; -import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart'; - -@RoutePage() -// ignore: must_be_immutable -/// Expects [currentAssetProvider] to be set before navigating to this page -class GalleryViewerPage extends HookConsumerWidget { - final int initialIndex; - final int heroOffset; - final bool showStack; - final RenderList renderList; - - GalleryViewerPage({ - super.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }) : controller = PageController(initialPage: initialIndex); - - final PageController controller; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final totalAssets = useState(renderList.totalAssets); - final isZoomed = useState(false); - final stackIndex = useState(0); - final localPosition = useRef(null); - final currentIndex = useValueNotifier(initialIndex); - final loadAsset = renderList.loadAsset; - final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final videoPlayerKeys = useRef>({}); - - GlobalKey getVideoPlayerKey(int id) { - videoPlayerKeys.value.putIfAbsent(id, () => GlobalKey()); - return videoPlayerKeys.value[id]!; - } - - Future precacheNextImage(int index) async { - if (!context.mounted) { - return; - } - - void onError(Object exception, StackTrace? stackTrace) { - // swallow error silently - log.severe('Error precaching next image: $exception, $stackTrace'); - } - - try { - if (index < totalAssets.value && index >= 0) { - final asset = loadAsset(index); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - context, - onError: onError, - ); - } - } catch (e) { - // swallow error silently - log.severe('Error precaching next image: $e'); - await context.maybePop(); - } - } - - useEffect(() { - if (ref.read(showControlsProvider)) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - } else { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - } - - // Delay this a bit so we can finish loading the page - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(currentIndex.value + 1); - }); - - return null; - }, const []); - - useEffect(() { - final asset = loadAsset(currentIndex.value); - - if (asset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(asset, false); - } else { - if (isCasting) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 1), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - }); - } - } - return null; - }, [ref.watch(castProvider).isCasting]); - - void showInfo() { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - showModalBottomSheet( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - barrierColor: Colors.transparent, - isScrollControlled: true, - showDragHandle: true, - enableDrag: true, - context: context, - useSafeArea: true, - builder: (context) { - return DraggableScrollableSheet( - minChildSize: 0.5, - maxChildSize: 1, - initialChildSize: 0.75, - expand: false, - builder: (context, scrollController) { - return Padding( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.advancedTroubleshooting) - ? AdvancedBottomSheet(assetDetail: asset, scrollController: scrollController) - : DetailPanel(asset: asset, scrollController: scrollController), - ); - }, - ); - }, - ); - } - - void handleSwipeUpDown(DragUpdateDetails details) { - const int sensitivity = 15; - const int dxThreshold = 50; - const double ratioThreshold = 3.0; - - if (isZoomed.value) { - return; - } - - // Guard [localPosition] null - if (localPosition.value == null) { - return; - } - - // Check for delta from initial down point - final d = details.localPosition - localPosition.value!; - // If the magnitude of the dx swipe is large, we probably didn't mean to go down - if (d.dx.abs() > dxThreshold) { - return; - } - - final ratio = d.dy / max(d.dx.abs(), 1); - if (d.dy > sensitivity && ratio > ratioThreshold) { - context.maybePop(); - } else if (d.dy < -sensitivity && ratio < -ratioThreshold) { - showInfo(); - } - } - - ref.listen(showControlsProvider, (_, show) { - if (show || Platform.isIOS) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - return; - } - - // This prevents the bottom bar from "dropping" while the controls are being hidden - Timer(const Duration(milliseconds: 100), () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - }); - }); - - PhotoViewGalleryPageOptions buildImage(Asset asset) { - return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __, ___) { - localPosition.value = details.localPosition; - }, - onDragUpdate: (_, details, __) { - handleSwipeUpDown(details); - }, - onTapDown: (ctx, tapDownDetails, _) { - final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); - if (!tapToNavigate) { - ref.read(showControlsProvider.notifier).toggle(); - return; - } - - double tapX = tapDownDetails.globalPosition.dx; - double screenWidth = ctx.width; - - // We want to change images if the user taps in the leftmost or - // rightmost quarter of the screen - bool tappedLeftSide = tapX < screenWidth / 4; - bool tappedRightSide = tapX > screenWidth * (3 / 4); - - int? currentPage = controller.page?.toInt(); - int maxPage = renderList.totalAssets - 1; - - if (tappedLeftSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != 0) { - controller.jumpToPage(currentPage - 1); - } - } else if (tappedRightSide && currentPage != null) { - // Nested if because we don't want to fallback to show/hide controls - if (currentPage != maxPage) { - controller.jumpToPage(currentPage + 1); - } - } else { - ref.read(showControlsProvider.notifier).toggle(); - } - }, - onLongPressStart: asset.isMotionPhoto - ? (_, __, ___) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = true; - } - : null, - imageProvider: ImmichImage.imageProvider(asset: asset), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - tightMode: true, - initialScale: PhotoViewComputedScale.contained * 0.99, - minScale: PhotoViewComputedScale.contained * 0.99, - errorBuilder: (context, error, stackTrace) => ImmichImage(asset, fit: BoxFit.contain), - ); - } - - PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { - return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __, ___) => localPosition.value = details.localPosition, - onDragUpdate: (_, details, __) => handleSwipeUpDown(details), - heroAttributes: _getHeroAttributes(asset), - filterQuality: FilterQuality.high, - initialScale: PhotoViewComputedScale.contained * 0.99, - maxScale: 1.0, - minScale: PhotoViewComputedScale.contained * 0.99, - basePosition: Alignment.center, - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: getVideoPlayerKey(asset.id), - asset: asset, - image: Image( - key: ValueKey(asset), - image: ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height), - fit: BoxFit.contain, - height: context.height, - width: context.width, - alignment: Alignment.center, - ), - ), - ), - ); - } - - PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - var newAsset = loadAsset(index); - - final stackId = newAsset.stackId; - if (stackId != null && currentIndex.value == index) { - final stackElements = ref.read(assetStackStateProvider(newAsset.stackId!)); - if (stackIndex.value < stackElements.length) { - newAsset = stackElements.elementAt(stackIndex.value); - } - } - - if (newAsset.isImage && !isPlayingMotionVideo) { - return buildImage(newAsset); - } - return buildVideo(context, newAsset); - } - - return PopScope( - // Change immersive mode back to normal "edgeToEdge" mode - onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), - child: Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - PhotoViewGallery.builder( - key: const ValueKey('gallery'), - scaleStateChangedCallback: (state) { - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - - if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { - isZoomed.value = state != PhotoViewScaleState.initial; - ref.read(showControlsProvider.notifier).show = !isZoomed.value; - } - }, - gaplessPlayback: true, - loadingBuilder: (context, event, index) { - final asset = loadAsset(index); - return ClipRect( - child: Stack( - fit: StackFit.expand, - children: [ - BackdropFilter(filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10)), - ImmichThumbnail(key: ValueKey(asset), asset: asset, fit: BoxFit.contain), - ], - ), - ); - }, - pageController: controller, - scrollPhysics: isZoomed.value - ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in - : (Platform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics() // Use heavy physics for Android - ), - itemCount: totalAssets.value, - scrollDirection: Axis.horizontal, - onPageChanged: (value, _) { - final next = currentIndex.value < value ? value + 1 : value - 1; - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - final newAsset = loadAsset(value); - - currentIndex.value = value; - stackIndex.value = 0; - - ref.read(currentAssetProvider.notifier).set(newAsset); - - // Wait for page change animation to finish, then precache the next image - Timer(const Duration(milliseconds: 400), () { - precacheNextImage(next); - }); - - context.scaffoldMessenger.hideCurrentSnackBar(); - - // send image to casting if the server has it - if (newAsset.isRemote) { - ref.read(castProvider.notifier).loadMediaOld(newAsset, false); - } else { - context.scaffoldMessenger.clearSnackBars(); - - if (isCasting) { - ref.read(castProvider.notifier).stop(); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - "local_asset_cast_failed".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - } - } - }, - builder: buildAsset, - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: GalleryAppBar(key: const ValueKey('app-bar'), showInfo: showInfo), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Column( - children: [ - GalleryStackedChildren(stackIndex), - BottomGalleryBar( - key: const ValueKey('bottom-bar'), - renderList: renderList, - totalAssets: totalAssets, - controller: controller, - showStack: showStack, - stackIndex: stackIndex, - assetIndex: currentIndex, - ), - ], - ), - ), - const DownloadPanel(), - ], - ), - ), - ); - } - - @pragma('vm:prefer-inline') - PhotoViewHeroAttributes _getHeroAttributes(Asset asset) { - return PhotoViewHeroAttributes( - tag: asset.isInDb ? asset.id + heroOffset : '${asset.remoteId}-$heroOffset', - transitionOnUserGestures: true, - ); - } -} diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart deleted file mode 100644 index b1eed29c5c..0000000000 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; -import 'package:logging/logging.dart'; -import 'package:native_video_player/native_video_player.dart'; - -@RoutePage() -class NativeVideoViewerPage extends HookConsumerWidget { - static final log = Logger('NativeVideoViewer'); - final Asset asset; - final bool showControls; - final int playbackDelayFactor; - final Widget image; - - const NativeVideoViewerPage({ - super.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final videoId = asset.id.toString(); - final controller = useState(null); - final shouldPlayOnForeground = useRef(true); - - final currentAsset = useState(ref.read(currentAssetProvider)); - final isCurrent = currentAsset.value == asset; - - // Used to show the placeholder during hero animations for remote videos to avoid a stutter - final isVisible = useState(Platform.isIOS && asset.isLocal); - - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - final isVideoReady = useState(false); - - Future createSource() async { - if (!context.mounted) { - return null; - } - - try { - final local = asset.local; - if (local != null && asset.livePhotoVideoId == null) { - final file = await local.file; - if (file == null) { - throw Exception('No file found for the video'); - } - - final source = await VideoSource.init(path: file.path, type: VideoSourceType.file); - return source; - } - - // Use a network URL for the video player controller - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref - .read(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.loadOriginalVideo); - final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; - final String videoUrl = asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' - : '$serverEndpoint/assets/${asset.remoteId}/$postfixUrl'; - - final source = await VideoSource.init( - path: videoUrl, - type: VideoSourceType.network, - headers: ApiService.getRequestHeaders(), - ); - return source; - } catch (error) { - log.severe('Error creating video source for asset ${asset.fileName}: $error'); - return null; - } - } - - final videoSource = useMemoized>(() => createSource()); - final aspectRatio = useState(asset.aspectRatio); - useMemoized(() async { - if (!context.mounted || aspectRatio.value != null) { - return null; - } - - try { - aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset); - } catch (error) { - log.severe('Error getting aspect ratio for asset ${asset.fileName}: $error'); - } - }); - - void onPlaybackReady() async { - final videoController = controller.value; - if (videoController == null || !isCurrent || !context.mounted) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.onNativePlaybackReady(); - - isVideoReady.value = true; - - try { - final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.autoPlayVideo); - if (autoPlayVideo) { - await notifier.play(); - } - await notifier.setVolume(1); - } catch (error) { - log.severe('Error playing video: $error'); - } - } - - void onPlaybackStatusChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativeStatusChanged(); - } - - void onPlaybackPositionChanged() { - if (!context.mounted) return; - ref.read(videoPlayerProvider(videoId).notifier).onNativePositionChanged(); - } - - void onPlaybackEnded() { - if (!context.mounted) return; - - ref.read(videoPlayerProvider(videoId).notifier).onNativePlaybackEnded(); - - final videoController = controller.value; - if (videoController?.playbackInfo?.status == PlaybackStatus.stopped && - !ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo)) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - } - } - - void removeListeners(NativeVideoPlayerController controller) { - controller.onPlaybackPositionChanged.removeListener(onPlaybackPositionChanged); - controller.onPlaybackStatusChanged.removeListener(onPlaybackStatusChanged); - controller.onPlaybackReady.removeListener(onPlaybackReady); - controller.onPlaybackEnded.removeListener(onPlaybackEnded); - } - - void initController(NativeVideoPlayerController nc) async { - if (controller.value != null || !context.mounted) { - return; - } - - final source = await videoSource; - if (source == null) { - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - notifier.attachController(nc); - - nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); - nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); - nc.onPlaybackReady.addListener(onPlaybackReady); - nc.onPlaybackEnded.addListener(onPlaybackEnded); - - unawaited( - nc.loadVideoSource(source).catchError((error) { - log.severe('Error loading video source: $error'); - }), - ); - final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); - await notifier.setLoop(loopVideo); - - controller.value = nc; - } - - ref.listen(currentAssetProvider, (_, value) { - final playerController = controller.value; - if (playerController != null && value != asset) { - removeListeners(playerController); - } - - final curAsset = currentAsset.value; - if (curAsset == asset) { - return; - } - - final imageToVideo = curAsset != null && !curAsset.isVideo; - - // No need to delay video playback when swiping from an image to a video - if (imageToVideo && Platform.isIOS) { - currentAsset.value = value; - onPlaybackReady(); - return; - } - - // Delay the video playback to avoid a stutter in the swipe animation - Timer( - Platform.isIOS - ? Duration(milliseconds: 300 * playbackDelayFactor) - : imageToVideo - ? Duration(milliseconds: 200 * playbackDelayFactor) - : Duration(milliseconds: 400 * playbackDelayFactor), - () { - if (!context.mounted) { - return; - } - - currentAsset.value = value; - if (currentAsset.value == asset) { - onPlaybackReady(); - } - }, - ); - }); - - useEffect(() { - // If opening a remote video from a hero animation, delay visibility to avoid a stutter - final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true); - - return () { - timer?.cancel(); - final playerController = controller.value; - if (playerController == null) { - return; - } - removeListeners(playerController); - playerController.stop().catchError((error) { - log.fine('Error stopping video: $error'); - }); - }; - }, const []); - - useOnAppLifecycleStateChange((_, state) async { - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { - await notifier.play(); - } else if (state == AppLifecycleState.paused) { - final videoPlaying = await controller.value?.isPlaying(); - if (videoPlaying ?? true) { - shouldPlayOnForeground.value = true; - await notifier.pause(); - } else { - shouldPlayOnForeground.value = false; - } - } - }); - - return Stack( - children: [ - // This remains under the video to avoid flickering - // For motion videos, this is the image portion of the asset - if (!isVideoReady.value || asset.isMotionPhoto) Center(key: ValueKey(asset.id), child: image), - if (aspectRatio.value != null && !isCasting) - Visibility.maintain( - key: ValueKey(asset), - visible: isVisible.value, - child: Center( - key: ValueKey(asset), - child: AspectRatio( - key: ValueKey(asset), - aspectRatio: aspectRatio.value!, - child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null, - ), - ), - ), - if (showControls) Center(child: CustomVideoPlayerControls(videoId: videoId)), - ], - ); - } -} diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index e8f5eb2ee2..65970ee294 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -2,14 +2,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/free_up_space_settings.dart'; @@ -38,8 +35,7 @@ enum SettingSection { Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), - SettingSection.backup => - Store.tryGet(StoreKey.betaTimeline) ?? false ? const DriftBackupSettings() : const BackupSettings(), + SettingSection.backup => const DriftBackupSettings(), SettingSection.freeUpSpace => const FreeUpSpaceSettings(), SettingSection.languages => const LanguageSettings(), SettingSection.networking => const NetworkingSettings(), @@ -74,13 +70,12 @@ class _MobileLayout extends StatelessWidget { .expand( (setting) => setting == SettingSection.beta ? [ - if (Store.isBetaTimelineEnabled) - SettingsCard( - icon: Icons.sync_outlined, - title: 'sync_status'.tr(), - subtitle: 'sync_status_subtitle'.tr(), - settingRoute: const SyncStatusRoute(), - ), + SettingsCard( + icon: Icons.sync_outlined, + title: 'sync_status'.tr(), + subtitle: 'sync_status_subtitle'.tr(), + settingRoute: const SyncStatusRoute(), + ), ] : [ SettingsCard( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 37c6b95806..725f7f9e85 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -12,13 +12,9 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -27,6 +23,8 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; class BootstrapErrorWidget extends StatelessWidget { @@ -323,29 +321,27 @@ class SplashScreenPageState extends ConsumerState { wsProvider.connect(); unawaited(infoProvider.getServerInfo()); - if (Store.isBetaTimelineEnabled) { - bool syncSuccess = false; + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); + + if (syncSuccess) { await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), ]); + } else { + await backgroundManager.hashAssets(); + } - if (syncSuccess) { - await Future.wait([ - backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); - }), - _resumeBackup(backupProvider), - // TODO: Bring back when the soft freeze issue is addressed - // backgroundManager.syncCloudIds(), - ]); - } else { - await backgroundManager.hashAssets(); - } - - if (Store.get(StoreKey.syncAlbums, false)) { - await backgroundManager.syncLinkedAlbum(); - } + if (Store.get(StoreKey.syncAlbums, false)) { + await backgroundManager.syncLinkedAlbum(); } } catch (e) { log.severe('Failed establishing connection to the server: $e'); @@ -368,58 +364,7 @@ class SplashScreenPageState extends ConsumerState { // clean install - change the default of the flag // current install not using beta timeline if (context.router.current.name == SplashScreenRoute.name) { - final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); - if (needBetaMigration) { - bool migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("New Timeline Experience"), - content: const Text( - "The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - if (migrate != true) { - migrate = - (await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Are you sure?"), - content: const Text( - "If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?", - ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), - ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), - ], - ), - )) ?? - false; - } - await Store.put(StoreKey.needBetaMigration, false); - if (migrate) { - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); - return; - } - } - - unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); - } - - if (Store.isBetaTimelineEnabled) { - return; - } - - final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; - if (hasPermission) { - // Resume backup (if enable) then navigate - await ref.watch(backupProvider.notifier).resumeBackup(); + unawaited(context.replaceRoute(const TabShellRoute())); } } diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart deleted file mode 100644 index ef637ba1c8..0000000000 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -@RoutePage() -class TabControllerPage extends HookConsumerWidget { - const TabControllerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRefreshingAssets = ref.watch(assetProvider); - final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider); - final isScreenLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; - - Widget buildIcon({required Widget icon, required bool isProcessing}) { - if (!isProcessing) return icon; - return Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - icon, - Positioned( - right: -18, - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(context.primaryColor), - ), - ), - ), - ], - ); - } - - void onNavigationSelected(TabsRouter router, int index) { - // On Photos page menu tapped - if (router.activeIndex == 0 && index == 0) { - scrollToTopNotifierProvider.scrollToTop(); - } - - // On Search page tapped - if (router.activeIndex == 1 && index == 1) { - ref.read(searchInputFocusProvider).requestFocus(); - } - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - router.setActiveIndex(index); - ref.read(tabProvider.notifier).state = TabEnum.values[index]; - } - - final navigationDestinations = [ - NavigationDestination( - label: 'photos'.tr(), - icon: const Icon(Icons.photo_library_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.photo_library, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'search'.tr(), - icon: const Icon(Icons.search_rounded), - selectedIcon: Icon(Icons.search, color: context.primaryColor), - ), - NavigationDestination( - label: 'albums'.tr(), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingRemoteAlbums, - icon: Icon(Icons.photo_album_rounded, color: context.primaryColor), - ), - ), - NavigationDestination( - label: 'library'.tr(), - icon: const Icon(Icons.space_dashboard_outlined), - selectedIcon: buildIcon( - isProcessing: isRefreshingAssets, - icon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor), - ), - ), - ]; - - Widget bottomNavigationBar(TabsRouter tabsRouter) { - return NavigationBar( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - destinations: navigationDestinations, - ); - } - - Widget navigationRail(TabsRouter tabsRouter) { - return NavigationRail( - destinations: navigationDestinations - .map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon)) - .toList(), - onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index), - selectedIndex: tabsRouter.activeIndex, - labelType: NavigationRailLabelType.all, - groupAlignment: 0.0, - ); - } - - final multiselectEnabled = ref.watch(multiselectProvider); - return AutoTabsRouter( - routes: [const PhotosRoute(), SearchRoute(), const AlbumsRoute(), const LibraryRoute()], - duration: const Duration(milliseconds: 600), - transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child), - builder: (context, child) { - final tabsRouter = AutoTabsRouter.of(context); - return PopScope( - canPop: tabsRouter.activeIndex == 0, - onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, - child: Scaffold( - resizeToAvoidBottomInset: false, - body: isScreenLandscape - ? Row( - children: [ - navigationRail(tabsRouter), - const VerticalDivider(), - Expanded(child: child), - ], - ) - : child, - bottomNavigationBar: multiselectEnabled || isScreenLandscape ? null : bottomNavigationBar(tabsRouter), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart deleted file mode 100644 index a6a66c1358..0000000000 --- a/mobile/lib/pages/editing/crop.page.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class CropImagePage extends HookWidget { - final Image image; - final Asset asset; - const CropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateLeft(); - }, - ), - IconButton( - icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color), - onPressed: () { - cropController.rotateRight(); - }, - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart deleted file mode 100644 index 2889785d0b..0000000000 --- a/mobile/lib/pages/editing/edit.page.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:typed_data'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class EditImagePage extends ConsumerWidget { - final Asset asset; - final Image image; - final bool isEdited; - - const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - Future _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - await ref - .read(fileMediaRepositoryProvider) - .saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg"); - await ref.read(albumProvider.notifier).refreshDeviceAlbums(); - context.navigator.popUntil((route) => route.isFirst); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!', gravity: ToastGravity.CENTER); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - gravity: ToastGravity.CENTER, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => context.navigator.popUntil((route) => route.isFirst), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(CropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(FilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/editing/filter.page.dart b/mobile/lib/pages/editing/filter.page.dart deleted file mode 100644 index f8b144bb96..0000000000 --- a/mobile/lib/pages/editing/filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class FilterImagePage extends HookWidget { - final Image image; - final Asset asset; - - const FilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/pages/library/archive.page.dart b/mobile/lib/pages/library/archive.page.dart deleted file mode 100644 index 8ca1bb9752..0000000000 --- a/mobile/lib/pages/library/archive.page.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class ArchivePage extends HookConsumerWidget { - const ArchivePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - final archiveRenderList = ref.watch(archiveTimelineProvider); - final count = archiveRenderList.value?.totalAssets.toString() ?? "?"; - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('archive_page_title').tr(namedArgs: {'count': count}), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: archiveTimelineProvider, - unarchive: true, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart deleted file mode 100644 index 649d7727d5..0000000000 --- a/mobile/lib/pages/library/favorite.page.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class FavoritesPage extends HookConsumerWidget { - const FavoritesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AppBar buildAppBar() { - return AppBar( - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('favorites').tr(), - ); - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : buildAppBar(), - body: MultiselectGrid( - renderListProvider: favoriteTimelineProvider, - favoriteEnabled: true, - editEnabled: true, - unfavorite: true, - ), - ); - } -} diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 497d3e5151..9de230d550 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -1,19 +1,22 @@ import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart'; import 'package:immich_mobile/providers/folder.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; RecursiveFolder? _findFolderInStructure(RootFolder rootFolder, RecursiveFolder targetFolder) { @@ -136,8 +139,8 @@ class FolderContent extends HookConsumerWidget { FolderPath(currentFolder: folder!, root: root), Expanded( child: folderRenderlist.when( - data: (list) { - if (folder!.subfolders.isEmpty && list.isEmpty) { + data: (folderAssets) { + if (folder!.subfolders.isEmpty && folderAssets.isEmpty) { return Center(child: const Text("empty_folder").tr()); } @@ -164,32 +167,33 @@ class FolderContent extends HookConsumerWidget { onTap: () => context.pushRoute(FolderRoute(folder: subfolder)), ), ), - if (!list.isEmpty && list.allAssets != null && list.allAssets!.isNotEmpty) - ...list.allAssets!.map( - (asset) => LargeLeadingTile( + if (folderAssets.isNotEmpty) + ...folderAssets.mapIndexed( + (index, asset) => LargeLeadingTile( onTap: () { - ref.read(currentAssetProvider.notifier).set(asset); + AssetViewer.setAsset(ref, asset); context.pushRoute( - GalleryViewerRoute(renderList: list, initialIndex: list.allAssets!.indexOf(asset)), + AssetViewerRoute( + initialIndex: index, + timelineService: ref + .read(timelineFactoryProvider) + .fromAssets(folderAssets, TimelineOrigin.folder), + ), ); }, leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(15)), - child: SizedBox( - width: 80, - height: 80, - child: ThumbnailImage(asset: asset, showStorageIndicator: false), - ), + child: SizedBox(width: 80, height: 80, child: ThumbnailTile(asset)), ), title: Text( - asset.fileName, + asset.name, maxLines: 2, softWrap: false, overflow: TextOverflow.ellipsis, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), subtitle: Text( - "${asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo?.fileSize ?? 0) : ""} • ${DateFormat.yMMMd().format(asset.fileCreatedAt)}", + "${asset.exifInfo.fileSize != null ? formatBytes(asset.exifInfo.fileSize ?? 0) : ""} • ${DateFormat.yMMMd().format(asset.createdAt)}", style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart deleted file mode 100644 index 99a534e9cf..0000000000 --- a/mobile/lib/pages/library/library.page.dart +++ /dev/null @@ -1,383 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class LibraryPage extends ConsumerWidget { - const LibraryPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - context.locale; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - - return Scaffold( - appBar: const ImmichAppBar(), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const FavoritesRoute()), - icon: Icons.favorite_outline_rounded, - label: context.t.favorites, - ), - const SizedBox(width: 8), - ActionButton( - onPressed: () => context.pushRoute(const ArchiveRoute()), - icon: Icons.archive_outlined, - label: context.t.archived, - ), - ], - ), - ), - const SizedBox(height: 8), - Row( - children: [ - ActionButton( - onPressed: () => context.pushRoute(const SharedLinkRoute()), - icon: Icons.link_outlined, - label: context.t.shared_links, - ), - SizedBox(width: trashEnabled ? 8 : 0), - trashEnabled - ? ActionButton( - onPressed: () => context.pushRoute(const TrashRoute()), - icon: Icons.delete_outline_rounded, - label: context.t.trash, - ) - : const SizedBox.shrink(), - ], - ), - const SizedBox(height: 12), - const Wrap( - spacing: 8, - runSpacing: 8, - children: [PeopleCollectionCard(), PlacesCollectionCard(), LocalAlbumsCollectionCard()], - ), - const SizedBox(height: 12), - const QuickAccessButtons(), - const SizedBox(height: 32), - ], - ), - ), - ); - } -} - -class QuickAccessButtons extends ConsumerWidget { - const QuickAccessButtons({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final partners = ref.watch(partnerSharedWithProvider); - - return Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(20), - topRight: const Radius.circular(20), - bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0), - bottomRight: Radius.circular(partners.isEmpty ? 20 : 0), - ), - ), - leading: const Icon(Icons.folder_outlined, size: 26), - title: Text(context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(FolderRoute()), - ), - ListTile( - leading: const Icon(Icons.lock_outline_rounded, size: 26), - title: Text( - context.t.locked_folder, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - onTap: () => context.pushRoute(const LockedRoute()), - ), - ListTile( - leading: const Icon(Icons.group_outlined, size: 26), - title: Text(context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: () => context.pushRoute(const PartnerRoute()), - ), - PartnerList(partners: partners), - ], - ), - ); - } -} - -class PartnerList extends ConsumerWidget { - const PartnerList({super.key, required this.partners}); - - final List partners; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: partners.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final partner = partners[index]; - final isLastItem = index == partners.length - 1; - return ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(isLastItem ? 20 : 0), - bottomRight: Radius.circular(isLastItem ? 20 : 0), - ), - ), - contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0), - leading: userAvatar(context, partner, radius: 16), - title: const Text( - "partner_list_user_photos", - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'user': partner.name}), - onTap: () => context.pushRoute((PartnerDetailRoute(partner: partner))), - ); - }, - ); - } -} - -class PeopleCollectionCard extends ConsumerWidget { - const PeopleCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const PeopleCollectionRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: people.widgetWhen( - onLoading: () => const Center(child: CircularProgressIndicator()), - onData: (people) { - return GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: people.take(4).map((person) { - return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); - }).toList(), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.people, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class LocalAlbumsCollectionCard extends HookConsumerWidget { - const LocalAlbumsCollectionCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(const LocalAlbumsRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { - return AlbumThumbnailCard(album: album, showTitle: false); - }).toList(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.on_this_device, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class PlacesCollectionCard extends StatelessWidget { - const PlacesCollectionCard({super.key}); - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final widthFactor = isTablet ? 0.25 : 0.5; - final size = context.width * widthFactor - 20.0; - - return GestureDetector( - onTap: () => context.pushRoute(PlacesCollectionRoute(currentLocation: null)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - color: context.colorScheme.secondaryContainer.withAlpha(100), - ), - child: IgnorePointer( - child: MapThumbnail( - zoom: 8, - centre: const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.t.places, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class ActionButton extends StatelessWidget { - final VoidCallback onPressed; - final IconData icon; - final String label; - - const ActionButton({super.key, required this.onPressed, required this.icon, required this.label}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: FilledButton.icon( - onPressed: onPressed, - label: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(label, style: TextStyle(color: context.colorScheme.onSurface, fontSize: 15)), - ), - style: FilledButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - backgroundColor: context.colorScheme.surfaceContainerLow, - alignment: Alignment.centerLeft, - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(25)), - side: BorderSide(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - ), - ), - icon: Icon(icon, color: context.primaryColor), - ), - ); - } -} diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart deleted file mode 100644 index e52a8326df..0000000000 --- a/mobile/lib/pages/library/local_albums.page.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class LocalAlbumsPage extends HookConsumerWidget { - const LocalAlbumsPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(localAlbumsProvider); - - return Scaffold( - appBar: AppBar(title: Text('on_this_device'.tr())), - body: ListView.builder( - padding: const EdgeInsets.all(18.0), - itemCount: albums.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: LargeLeadingTile( - leadingPadding: const EdgeInsets.only(right: 16), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: ImmichThumbnail(asset: albums[index].thumbnail.value, width: 80, height: 80), - ), - title: Text( - albums[index].name, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: Text( - 'items_count'.t(context: context, args: {'count': albums[index].assetCount}), - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - onTap: () => context.pushRoute(AlbumViewerRoute(albumId: albums[index].id)), - ), - ); - }, - ), - ); - } -} diff --git a/mobile/lib/pages/library/locked/locked.page.dart b/mobile/lib/pages/library/locked/locked.page.dart deleted file mode 100644 index aea62e0051..0000000000 --- a/mobile/lib/pages/library/locked/locked.page.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class LockedPage extends HookConsumerWidget { - const LockedPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appLifeCycle = useAppLifecycleState(); - final showOverlay = useState(false); - final authProviderNotifier = ref.read(authProvider.notifier); - // lock the page when it is destroyed - useEffect(() { - return () { - authProviderNotifier.lockPinCode(); - }; - }, []); - - useEffect(() { - if (context.mounted) { - if (appLifeCycle == AppLifecycleState.resumed) { - showOverlay.value = false; - } else { - showOverlay.value = true; - } - } - - return null; - }, [appLifeCycle]); - - return Scaffold( - appBar: ref.watch(multiselectProvider) ? null : const LockPageAppBar(), - body: showOverlay.value - ? const SizedBox() - : MultiselectGrid( - renderListProvider: lockedTimelineProvider, - topWidget: Padding( - padding: const EdgeInsets.all(16.0), - child: Center(child: Text('no_locked_photos_message'.tr(), style: context.textTheme.labelLarge)), - ), - editEnabled: false, - favoriteEnabled: false, - unfavorite: false, - archiveEnabled: false, - stackEnabled: false, - unarchive: false, - ), - ); - } -} - -class LockPageAppBar extends ConsumerWidget implements PreferredSizeWidget { - const LockPageAppBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AppBar( - leading: IconButton( - onPressed: () { - ref.read(authProvider.notifier).lockPinCode(); - context.maybePop(); - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - centerTitle: true, - automaticallyImplyLeading: false, - title: const Text('locked_folder').tr(), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index a39c91871b..3af320dc5f 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -22,7 +21,6 @@ class PinAuthPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final localAuthState = ref.watch(localAuthProvider); final showPinRegistrationForm = useState(createPinCode); - final isBetaTimeline = Store.isBetaTimelineEnabled; Future registerBiometric(String pinCode) async { final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric(context, pinCode); @@ -36,11 +34,7 @@ class PinAuthPage extends HookConsumerWidget { ), ); - if (isBetaTimeline) { - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); - } else { - unawaited(context.replaceRoute(const LockedRoute())); - } + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); } } @@ -89,11 +83,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - if (isBetaTimeline) { - context.replaceRoute(const DriftLockedFolderRoute()); - } else { - context.replaceRoute(const LockedRoute()); - } + context.replaceRoute(const DriftLockedFolderRoute()); }, ), ), diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart deleted file mode 100644 index eae4228a2d..0000000000 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; - -@RoutePage() -class PartnerPage extends HookConsumerWidget { - const PartnerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final List partners = ref.watch(partnerSharedByProvider); - final availableUsers = ref.watch(partnerAvailableProvider); - - addNewUsersHandler() async { - final users = availableUsers.value; - if (users == null || users.isEmpty) { - ImmichToast.show(context: context, msg: "partner_page_no_more_users".tr()); - return; - } - - final selectedUser = await showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: const Text("partner_page_select_partner").tr(), - children: [ - for (UserDto u in users) - SimpleDialogOption( - onPressed: () => context.pop(u), - child: Row( - children: [ - Padding(padding: const EdgeInsets.only(right: 8), child: userAvatar(context, u)), - Text(u.name), - ], - ), - ), - ], - ); - }, - ); - if (selectedUser != null) { - final ok = await ref.read(partnerServiceProvider).addPartner(selectedUser); - if (ok) { - ref.invalidate(partnerSharedByProvider); - } else { - ImmichToast.show(context: context, msg: "partner_page_partner_add_failed".tr(), toastType: ToastType.error); - } - } - } - - onDeleteUser(UserDto u) { - return showDialog( - context: context, - builder: (BuildContext context) { - return ConfirmDialog( - title: "stop_photo_sharing", - content: "partner_page_stop_sharing_content".tr(namedArgs: {'partner': u.name}), - onOk: () => ref.read(partnerServiceProvider).removePartner(u), - ); - }, - ); - } - - buildUserList(List users) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 16.0), - child: Text( - "partner_page_shared_to_title", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), - ).tr(), - ), - if (users.isNotEmpty) - ListView.builder( - shrinkWrap: true, - itemCount: users.length, - itemBuilder: ((context, index) { - return ListTile( - leading: userAvatar(context, users[index]), - title: Text(users[index].email, style: context.textTheme.bodyLarge), - trailing: IconButton( - icon: const Icon(Icons.person_remove), - onPressed: () => onDeleteUser(users[index]), - ), - ); - }), - ), - if (users.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: const Text("partner_page_empty_message", style: TextStyle(fontSize: 14)).tr(), - ), - Align( - alignment: Alignment.center, - child: ElevatedButton.icon( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - label: const Text("add_partner").tr(), - ), - ), - ], - ), - ), - ], - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text("partners").tr(), - elevation: 0, - centerTitle: false, - actions: [ - IconButton( - onPressed: availableUsers.whenOrNull(data: (data) => addNewUsersHandler), - icon: const Icon(Icons.person_add), - tooltip: "add_partner".tr(), - ), - ], - ), - body: buildUserList(partners), - ); - } -} diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart deleted file mode 100644 index 1f15dab6a3..0000000000 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class PartnerDetailPage extends HookConsumerWidget { - const PartnerDetailPage({super.key, required this.partner}); - - final UserDto partner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final inTimeline = useState(partner.inTimeline); - bool toggleInProcess = false; - - useEffect(() { - Future.microtask(() async => {await ref.read(assetProvider.notifier).getAllAsset()}); - return null; - }, []); - - void toggleInTimeline() async { - if (toggleInProcess) return; - toggleInProcess = true; - try { - final ok = await ref - .read(partnerSharedWithProvider.notifier) - .updatePartner(partner, inTimeline: !inTimeline.value); - if (ok) { - inTimeline.value = !inTimeline.value; - final action = inTimeline.value ? "shown on" : "hidden from"; - ImmichToast.show( - context: context, - toastType: ToastType.success, - durationInSecond: 1, - msg: "${partner.name}'s assets $action your timeline", - ); - } else { - ImmichToast.show( - context: context, - toastType: ToastType.error, - durationInSecond: 1, - msg: "Failed to toggle the timeline setting", - ); - } - } finally { - toggleInProcess = false; - } - } - - return Scaffold( - appBar: ref.watch(multiselectProvider) - ? null - : AppBar(title: Text(partner.name), elevation: 0, centerTitle: false), - body: MultiselectGrid( - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - colors: [context.colorScheme.primary.withAlpha(10), context.colorScheme.primary.withAlpha(15)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ListTile( - title: Text( - "Show in timeline", - style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary), - ), - subtitle: Text( - "Show photos and videos from this user in your timeline", - style: context.textTheme.bodyMedium, - ), - trailing: Switch(value: inTimeline.value, onChanged: (_) => toggleInTimeline()), - ), - ), - ), - ), - renderListProvider: singleUserTimelineProvider(partner.id), - onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), - deleteEnabled: false, - favoriteEnabled: false, - ), - ); - } -} diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart deleted file mode 100644 index bff52df6da..0000000000 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -@RoutePage() -class PeopleCollectionPage extends HookConsumerWidget { - const PeopleCollectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(getAllPeopleProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - showNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - return LayoutBuilder( - builder: (context, constraints) { - final isTablet = constraints.maxWidth > 600; - final isPortrait = context.orientation == Orientation.portrait; - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - focusNode: formFocus, - onTapOutside: (_) => formFocus.unfocus(), - onChanged: (value) => search.value = value, - filled: true, - hintText: 'filter_people'.tr(), - autofocus: true, - ) - : Text('people'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: SafeArea( - child: people.when( - data: (people) { - if (search.value != null) { - people = people.where((person) { - return person.name.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 6 : 3, - childAspectRatio: 0.85, - mainAxisSpacing: isPortrait && isTablet ? 36 : 0, - ), - padding: const EdgeInsets.symmetric(vertical: 32), - itemCount: people.length, - itemBuilder: (context, index) { - final person = people[index]; - - return Column( - children: [ - GestureDetector( - onTap: () { - context.pushRoute(PersonResultRoute(personId: person.id, personName: person.name)); - }, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: isTablet ? 120 / 2 : 96 / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () => showNameEditModel(person.id, person.name), - child: person.name.isEmpty - ? Text( - 'add_a_name'.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.primary, - ), - ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - person.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), - ), - ), - ], - ); - }, - ); - }, - error: (error, stack) => const Text("error"), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ), - ); - }, - ); - } -} diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart deleted file mode 100644 index a4a6f66915..0000000000 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class PlacesCollectionPage extends HookConsumerWidget { - const PlacesCollectionPage({super.key, this.currentLocation}); - final LatLng? currentLocation; - @override - Widget build(BuildContext context, WidgetRef ref) { - final places = ref.watch(getAllPlacesProvider); - final formFocus = useFocusNode(); - final ValueNotifier search = useState(null); - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: search.value == null, - title: search.value != null - ? SearchField( - autofocus: true, - filled: true, - focusNode: formFocus, - onChanged: (value) => search.value = value, - onTapOutside: (_) => formFocus.unfocus(), - hintText: 'filter_places'.tr(), - ) - : Text('places'.tr()), - actions: [ - IconButton( - icon: Icon(search.value != null ? Icons.close : Icons.search), - onPressed: () { - search.value = search.value == null ? '' : null; - }, - ), - ], - ), - body: ListView( - shrinkWrap: true, - children: [ - if (search.value == null) - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - height: 200, - width: context.width, - child: MapThumbnail( - onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), - zoom: 8, - centre: currentLocation ?? const LatLng(21.44950, -157.91959), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - places.when( - data: (places) { - if (search.value != null) { - places = places.where((place) { - return place.label.toLowerCase().contains(search.value!.toLowerCase()); - }).toList(); - } - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: places.length, - itemBuilder: (context, index) { - final place = places[index]; - - return PlaceTile(id: place.id, name: place.label); - }, - ); - }, - error: (error, stask) => Text('error_getting_places'.tr()), - loading: () => const Center(child: CircularProgressIndicator()), - ), - ], - ), - ); - } -} - -class PlaceTile extends StatelessWidget { - const PlaceTile({super.key, required this.id, required this.name}); - - final String id; - final String name; - - @override - Widget build(BuildContext context) { - final thumbnailUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; - - void navigateToPlace() { - context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: name), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - } - - return LargeLeadingTile( - onTap: () => navigateToPlace(), - title: Text(name, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: SizedBox( - width: 80, - height: 80, - child: Thumbnail(imageProvider: RemoteImageProvider(url: thumbnailUrl)), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart deleted file mode 100644 index 2279998c2d..0000000000 --- a/mobile/lib/pages/library/trash.page.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -@RoutePage() -class TrashPage extends HookConsumerWidget { - const TrashPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final trashRenderList = ref.watch(trashTimelineProvider); - final trashDays = ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); - final selectionEnabledHook = useState(false); - final selection = useState({}); - final processing = useProcessingOverlay(); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - } - - onEmptyTrash() async { - processing.value = true; - await ref.read(trashProvider.notifier).emptyTrash(); - processing.value = false; - selectionEnabledHook.value = false; - if (context.mounted) { - ImmichToast.show(context: context, msg: 'trash_emptied'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleEmptyTrash() async { - await showDialog( - context: context, - builder: (context) => ConfirmDialog( - onOk: () => onEmptyTrash(), - title: "empty_trash".tr(), - ok: "ok".tr(), - content: "trash_page_empty_trash_dialog_content".tr(), - ), - ); - } - - Future onPermanentlyDelete() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final isRemoved = await ref.read(assetProvider.notifier).deleteAssets(selection.value, force: true); - - if (isRemoved) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - handlePermanentDelete() async { - await showDialog( - context: context, - builder: (context) => DeleteDialog(alert: "delete_dialog_alert_remote", onDelete: () => onPermanentlyDelete()), - ); - } - - Future handleRestoreAll() async { - processing.value = true; - await ref.read(trashProvider.notifier).restoreTrash(); - processing.value = false; - selectionEnabledHook.value = false; - } - - Future handleRestore() async { - processing.value = true; - try { - if (selection.value.isNotEmpty) { - final result = await ref.read(trashProvider.notifier).restoreAssets(selection.value); - - if (result && context.mounted) { - ImmichToast.show( - context: context, - msg: 'assets_restored_successfully'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - String getAppBarTitle(String count) { - if (selectionEnabledHook.value) { - return selection.value.isNotEmpty ? "${selection.value.length}" : "trash_page_select_assets_btn".tr(); - } - return 'trash_page_title'.tr(namedArgs: {'count': count}); - } - - AppBar buildAppBar(String count) { - return AppBar( - leading: IconButton( - onPressed: !selectionEnabledHook.value - ? () => context.maybePop() - : () { - selectionEnabledHook.value = false; - selection.value = {}; - }, - icon: !selectionEnabledHook.value - ? const Icon(Icons.arrow_back_ios_rounded) - : const Icon(Icons.close_rounded), - ), - centerTitle: !selectionEnabledHook.value, - automaticallyImplyLeading: false, - title: Text(getAppBarTitle(count)), - actions: [ - if (!selectionEnabledHook.value) - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem(value: () => selectionEnabledHook.value = true, child: const Text('select').tr()), - PopupMenuItem(value: handleEmptyTrash, child: const Text('empty_trash').tr()), - ]; - }, - onSelected: (fn) => fn(), - ), - ], - ); - } - - Widget buildBottomBar() { - return SafeArea( - child: Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 64, - child: Container( - color: context.themeData.canvasColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - icon: Icon(Icons.delete_forever, color: Colors.red[400]), - label: Text( - selection.value.isEmpty ? 'trash_page_delete_all'.tr() : 'delete'.tr(), - style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleEmptyTrash - : handlePermanentDelete, - ), - TextButton.icon( - icon: const Icon(Icons.history_rounded), - label: Text( - selection.value.isEmpty ? 'trash_page_restore_all'.tr() : 'restore'.tr(), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), - onPressed: processing.value - ? null - : selection.value.isEmpty - ? handleRestoreAll - : handleRestore, - ), - ], - ), - ), - ), - ), - ); - } - - return Scaffold( - appBar: trashRenderList.maybeWhen( - orElse: () => buildAppBar("?"), - data: (data) => buildAppBar(data.totalAssets.toString()), - ), - body: trashRenderList.widgetWhen( - onData: (data) => data.isEmpty - ? Center(child: Text('trash_page_no_assets'.tr())) - : Stack( - children: [ - SafeArea( - child: ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - showMultiSelectIndicator: false, - showStack: true, - topWidget: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24), - child: const Text("trash_page_info").tr(namedArgs: {"days": "$trashDays"}), - ), - ), - ), - if (selectionEnabledHook.value) buildBottomBar(), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/onboarding/permission_onboarding.page.dart b/mobile/lib/pages/onboarding/permission_onboarding.page.dart deleted file mode 100644 index 52d4ac0125..0000000000 --- a/mobile/lib/pages/onboarding/permission_onboarding.page.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_logo.dart'; -import 'package:immich_mobile/widgets/common/immich_title_text.dart'; -import 'package:permission_handler/permission_handler.dart'; - -@RoutePage() -class PermissionOnboardingPage extends HookConsumerWidget { - const PermissionOnboardingPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final PermissionStatus permission = ref.watch(galleryPermissionNotifier); - - // Navigate to the main Tab Controller when permission is granted - void goToBackup() => context.replaceRoute(const BackupControllerRoute()); - - // When the permission is denied, we show a request permission page - buildRequestPermission() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('permission_onboarding_request', style: context.textTheme.titleMedium, textAlign: TextAlign.center).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => - ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission().then((permission) async { - if (permission.isGranted) { - // If permission is limited, we will show the limited - // permission page - goToBackup(); - } - }), - child: const Text('continue').tr(), - ), - ], - ); - } - - // When permission is granted from outside the app, this will show to - // let them continue on to the main timeline - buildPermissionGranted() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'permission_onboarding_permission_granted', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_get_started').tr()), - ], - ); - } - - // iOS 14+ has limited permission options, which let someone just share - // a few photos with the app. If someone only has limited permissions, we - // inform that Immich works best when given full permission - buildPermissionLimited() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.yellow, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_limited', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - const SizedBox(height: 8.0), - TextButton(onPressed: () => goToBackup(), child: const Text('permission_onboarding_continue_anyway').tr()), - ], - ); - } - - buildPermissionDenied() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_outlined, color: Colors.red, size: 48), - const SizedBox(height: 8), - Text( - 'permission_onboarding_permission_denied', - style: context.textTheme.titleMedium, - textAlign: TextAlign.center, - ).tr(), - const SizedBox(height: 18), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text('permission_onboarding_go_to_settings').tr(), - ), - ], - ); - } - - final Widget child = switch (permission) { - PermissionStatus.limited => buildPermissionLimited(), - PermissionStatus.denied => buildRequestPermission(), - PermissionStatus.granted || PermissionStatus.provisional => buildPermissionGranted(), - PermissionStatus.restricted || PermissionStatus.permanentlyDenied => buildPermissionDenied(), - }; - - return Scaffold( - body: SafeArea( - child: Center( - child: SizedBox( - width: 380, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLogo(heroTag: 'logo'), - const ImmichTitleText(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Padding(padding: const EdgeInsets.all(18.0), child: child), - ), - TextButton(child: const Text('back').tr(), onPressed: () => context.maybePop()), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart deleted file mode 100644 index bd7973bc21..0000000000 --- a/mobile/lib/pages/photos/memory.page.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart'; -import 'package:immich_mobile/widgets/memories/memory_card.dart'; -import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; -import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; - -@RoutePage() -/// Expects [currentAssetProvider] to be set before navigating to this page -class MemoryPage extends HookConsumerWidget { - final List memories; - final int memoryIndex; - - const MemoryPage({required this.memories, required this.memoryIndex, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentMemory = useState(memories[memoryIndex]); - final currentAssetPage = useState(0); - final currentMemoryIndex = useState(memoryIndex); - final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"); - const bgColor = Colors.black; - final currentAsset = useState(null); - - /// The list of all of the asset page controllers - final memoryAssetPageControllers = List.generate(memories.length, (i) => usePageController()); - - /// The main vertically scrolling page controller with each list of memories - final memoryPageController = usePageController(initialPage: memoryIndex); - - useEffect(() { - // Memories is an immersive activity - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - return () { - // Clean up to normal edge to edge when we are done - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }; - }); - - toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - } - - void toPreviousMemory() { - if (currentMemoryIndex.value > 0) { - // Move to the previous memory page - memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); - - // Wait for the next frame to ensure the page is built - SchedulerBinding.instance.addPostFrameCallback((_) { - final previousIndex = currentMemoryIndex.value - 1; - final previousMemoryController = memoryAssetPageControllers[previousIndex]; - - // Ensure the controller is attached - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } else { - // Wait for the next frame until it is attached - SchedulerBinding.instance.addPostFrameCallback((_) { - if (previousMemoryController.hasClients) { - previousMemoryController.jumpToPage(memories[previousIndex].assets.length - 1); - } - }); - } - }); - } - } - - toNextAsset(int currentAssetIndex) { - if (currentAssetIndex + 1 < currentMemory.value.assets.length) { - // Go to the next asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the next memory since we are at the end of our assets - toNextMemory(); - } - } - - toPreviousAsset(int currentAssetIndex) { - if (currentAssetIndex > 0) { - // Go to the previous asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - - controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); - } else { - // Go to the previous memory since we are at the end of our assets - toPreviousMemory(); - } - } - - updateProgressText() { - assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; - } - - /// Downloads and caches the image for the asset at this [currentMemory]'s index - precacheAsset(int index) async { - // Guard index out of range - if (index < 0) { - return; - } - - // Context might be removed due to popping out of Memory Lane during Scroll handling - if (!context.mounted) { - return; - } - - late Asset asset; - if (index < currentMemory.value.assets.length) { - // Uses the next asset in this current memory - asset = currentMemory.value.assets[index]; - } else { - // Precache the first asset in the next memory if available - final currentMemoryIndex = memories.indexOf(currentMemory.value); - - // Guard no memory found - if (currentMemoryIndex == -1) { - return; - } - - final nextMemoryIndex = currentMemoryIndex + 1; - // Guard no next memory - if (nextMemoryIndex >= memories.length) { - return; - } - - // Get the first asset from the next memory - asset = memories[nextMemoryIndex].assets.first; - } - - // Precache the asset - final size = MediaQuery.sizeOf(context); - await precacheImage( - ImmichImage.imageProvider(asset: asset, width: size.width, height: size.height), - context, - size: size, - ); - } - - // Precache the next page right away if we are on the first page - if (currentAssetPage.value == 0) { - Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); - } - - Future onAssetChanged(int otherIndex) async { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - currentAssetPage.value = otherIndex; - updateProgressText(); - - // Wait for page change animation to finish - await Future.delayed(const Duration(milliseconds: 400)); - // And then precache the next asset - await precacheAsset(otherIndex + 1); - - final asset = currentMemory.value.assets[otherIndex]; - currentAsset.value = asset; - ref.read(currentAssetProvider.notifier).set(asset); - } - - /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called - * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final - * page during the end of scroll is different than the current page - */ - return NotificationListener( - onNotification: (ScrollNotification notification) { - // Calculate OverScroll manually using the number of pixels away from maxScrollExtent - // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 - // or sum of vertical pixels of all memories for depth = 0 - if (notification is ScrollUpdateNotification) { - final isEpiloguePage = (memoryPageController.page?.floor() ?? 0) >= memories.length; - - final offset = notification.metrics.pixels; - if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { - context.maybePop(); - return true; - } - } - - return false; - }, - child: Scaffold( - backgroundColor: bgColor, - body: SafeArea( - child: PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - scrollDirection: Axis.vertical, - controller: memoryPageController, - onPageChanged: (pageNumber) { - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - if (pageNumber < memories.length) { - currentMemoryIndex.value = pageNumber; - currentMemory.value = memories[pageNumber]; - } - - currentAssetPage.value = 0; - - updateProgressText(); - }, - itemCount: memories.length + 1, - itemBuilder: (context, mIndex) { - // Build last page - if (mIndex == memories.length) { - return MemoryEpilogue( - onStartOver: () => memoryPageController.animateToPage( - 0, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ), - ); - } - // Build horizontal page - final assetController = memoryAssetPageControllers[mIndex]; - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 24.0, right: 24.0, top: 8.0, bottom: 2.0), - child: AnimatedBuilder( - animation: assetController, - builder: (context, child) { - double value = 0.0; - if (assetController.hasClients) { - // We can only access [page] if this has clients - value = assetController.page ?? 0; - } - return MemoryProgressIndicator( - ticks: memories[mIndex].assets.length, - value: (value + 1) / memories[mIndex].assets.length, - ); - }, - ), - ), - Expanded( - child: Stack( - children: [ - PageView.builder( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - controller: assetController, - onPageChanged: onAssetChanged, - scrollDirection: Axis.horizontal, - itemCount: memories[mIndex].assets.length, - itemBuilder: (context, index) { - final asset = memories[mIndex].assets[index]; - return Stack( - children: [ - Container( - color: Colors.black, - child: MemoryCard(asset: asset, title: memories[mIndex].title, showTitle: index == 0), - ), - Positioned.fill( - child: Row( - children: [ - // Left side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toPreviousAsset(index); - }, - ), - ), - - // Right side of the screen - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - toNextAsset(index); - }, - ), - ), - ], - ), - ), - ], - ); - }, - ), - Positioned( - top: 8, - left: 8, - child: MaterialButton( - minWidth: 0, - onPressed: () { - // auto_route doesn't invoke pop scope, so - // turn off full screen mode here - // https://github.com/Milad-Akarie/auto_route_library/issues/1799 - context.maybePop(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.close_rounded, color: Colors.white), - ), - ), - if (currentAsset.value != null && currentAsset.value!.isVideo) - Positioned( - bottom: 24, - right: 32, - child: Icon(Icons.videocam_outlined, color: Colors.grey[200]), - ), - ], - ), - ), - MemoryBottomInfo(memory: memories[mIndex]), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart deleted file mode 100644 index 7f57247ec4..0000000000 --- a/mobile/lib/pages/photos/photos.page.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; -import 'package:immich_mobile/widgets/memories/memory_lane.dart'; - -@RoutePage() -class PhotosPage extends HookConsumerWidget { - const PhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentUser = ref.watch(currentUserProvider); - final timelineUsers = ref.watch(timelineUsersIdsProvider); - final tipOneOpacity = useState(0.0); - final refreshCount = useState(0); - - useEffect(() { - ref.read(websocketProvider.notifier).connect(); - Future(() => ref.read(assetProvider.notifier).getAllAsset()); - Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums()); - ref.read(serverInfoProvider.notifier).getServerInfo(); - - return; - }, []); - - Widget buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const ImmichLoadingIndicator(), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - 'home_page_building_timeline', - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ).tr(), - ), - const SizedBox(height: 8), - AnimatedOpacity( - duration: const Duration(milliseconds: 1000), - opacity: tipOneOpacity.value, - child: Column( - children: [ - SizedBox( - width: 320, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'home_page_first_time_notice', - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium, - ).tr(), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Future refreshAssets() async { - final fullRefresh = refreshCount.value > 0; - - if (fullRefresh) { - unawaited( - Future.wait([ - ref.read(assetProvider.notifier).getAllAsset(clear: true), - ref.read(albumProvider.notifier).refreshRemoteAlbums(), - ]), - ); - - // refresh was forced: user requested another refresh within 2 seconds - refreshCount.value = 0; - } else { - await ref.read(assetProvider.notifier).getAllAsset(clear: false); - - refreshCount.value++; - // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () => refreshCount.value = 0); - } - } - - return Stack( - children: [ - MultiselectGrid( - topWidget: (currentUser != null && currentUser.memoryEnabled) ? const MemoryLane() : const SizedBox(), - renderListProvider: timelineUsers.length > 1 - ? multiUsersTimelineProvider(timelineUsers) - : singleUserTimelineProvider(currentUser?.id), - buildLoadingIndicator: buildLoadingIndicator, - onRefresh: refreshAssets, - stackEnabled: true, - archiveEnabled: true, - editEnabled: true, - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - top: ref.watch(multiselectProvider) ? -(kToolbarHeight + context.padding.top) : 0, - left: 0, - right: 0, - child: Container( - height: kToolbarHeight + context.padding.top, - color: context.themeData.appBarTheme.backgroundColor, - child: const ImmichAppBar(), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/pages/search/all_motion_videos.page.dart b/mobile/lib/pages/search/all_motion_videos.page.dart deleted file mode 100644 index 60bb8a6cff..0000000000 --- a/mobile/lib/pages/search/all_motion_videos.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/all_motion_photos.provider.dart'; - -@RoutePage() -class AllMotionPhotosPage extends HookConsumerWidget { - const AllMotionPhotosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final motionPhotos = ref.watch(allMotionPhotosProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('search_page_motion_photos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: motionPhotos.widgetWhen(onData: (assets) => ImmichAssetGrid(assets: assets)), - ); - } -} diff --git a/mobile/lib/pages/search/all_people.page.dart b/mobile/lib/pages/search/all_people.page.dart deleted file mode 100644 index b2814e6c13..0000000000 --- a/mobile/lib/pages/search/all_people.page.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPeoplePage extends HookConsumerWidget { - const AllPeoplePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final curatedPeople = ref.watch(getAllPeopleProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('people').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: curatedPeople.widgetWhen( - onData: (people) => ExploreGrid( - isPeople: true, - curatedContent: people.map((e) => SearchCuratedContent(label: e.name, id: e.id)).toList(), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/all_places.page.dart b/mobile/lib/pages/search/all_places.page.dart deleted file mode 100644 index c92f87d3ac..0000000000 --- a/mobile/lib/pages/search/all_places.page.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/widgets/search/explore_grid.dart'; - -@RoutePage() -class AllPlacesPage extends HookConsumerWidget { - const AllPlacesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> places = ref.watch(getAllPlacesProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('places').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: places.widgetWhen(onData: (data) => ExploreGrid(curatedContent: data)), - ); - } -} diff --git a/mobile/lib/pages/search/all_videos.page.dart b/mobile/lib/pages/search/all_videos.page.dart deleted file mode 100644 index acad043a58..0000000000 --- a/mobile/lib/pages/search/all_videos.page.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; - -@RoutePage() -class AllVideosPage extends HookConsumerWidget { - const AllVideosPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('videos').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: MultiselectGrid(renderListProvider: allVideosTimelineProvider), - ); - } -} diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart deleted file mode 100644 index 993b91d8f7..0000000000 --- a/mobile/lib/pages/search/map/map.page.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/models/map/map_marker.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/map/map_marker.provider.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/debounce.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/map_utils.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/map/map_app_bar.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; -import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -@RoutePage() -class MapPage extends HookConsumerWidget { - const MapPage({super.key, this.initialLocation}); - final LatLng? initialLocation; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapController = useRef(null); - final markers = useRef>([]); - final markersInBounds = useRef>([]); - final bottomSheetStreamController = useStreamController(); - final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null); - final assetsDebouncer = useDebouncer(); - final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); - final isLoading = useProcessingOverlay(); - final scrollController = useScrollController(); - final markerDebouncer = useDebouncer(interval: const Duration(milliseconds: 800)); - final selectedAssets = useValueNotifier>({}); - const mapZoomToAssetLevel = 12.0; - - // updates the markersInBounds value with the map markers that are visible in the current - // map camera bounds - Future updateAssetsInBounds() async { - // Guard map not created - if (mapController.value == null) { - return; - } - - final bounds = await mapController.value!.getVisibleRegion(); - final inBounds = markers.value - .where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude))) - .toList(); - // Notify bottom sheet to update asset grid only when there are new assets - if (markersInBounds.value.length != inBounds.length) { - bottomSheetStreamController.add(MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList())); - } - markersInBounds.value = inBounds; - } - - // removes all sources and layers and re-adds them with the updated markers - Future reloadLayers() async { - if (mapController.value != null) { - layerDebouncer.run(() => mapController.value!.reloadAllLayersForMarkers(markers.value)); - } - } - - Future loadMarkers() async { - try { - isLoading.value = true; - markers.value = await ref.read(mapMarkersProvider.future); - assetsDebouncer.run(updateAssetsInBounds); - await reloadLayers(); - } finally { - isLoading.value = false; - } - } - - useEffect(() { - final currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - - loadMarkers(); - return currentAssetLink.close; - }, []); - - // Refetch markers when map state is changed - ref.listen(mapStateNotifierProvider, (_, current) { - if (current.shouldRefetchMarkers) { - markerDebouncer.run(() { - ref.invalidate(mapMarkersProvider); - // Reset marker - selectedMarker.value = null; - loadMarkers(); - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false); - }); - } - }); - - // updates the selected markers position based on the current map camera - Future updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async { - final assetPoint = await mapController.value!.toScreenLocation(marker.latLng); - selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate); - (assetPoint, marker, shouldAnimate); - } - - // finds the nearest asset marker from the tap point and store it as the selectedMarker - Future onMarkerClicked(Point point, LatLng _) async { - // Guard map not created - if (mapController.value == null) { - return; - } - final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50); - final marker = markersInBounds.value.firstWhereOrNull( - (m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)), - ); - - if (marker != null) { - await updateAssetMarkerPosition(marker); - } else { - // If no asset was previously selected and no new asset is available, close the bottom sheet - if (selectedMarker.value == null) { - bottomSheetStreamController.add(const MapCloseBottomSheet()); - } - selectedMarker.value = null; - } - } - - void onMapCreated(MapLibreMapController controller) async { - mapController.value = controller; - controller.addListener(() { - if (controller.isCameraMoving && selectedMarker.value != null) { - updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false); - } - }); - } - - Future onMarkerTapped() async { - final assetId = selectedMarker.value?.marker.assetRemoteId; - if (assetId == null) { - return; - } - - final asset = await ref.read(dbProvider).assets.getByRemoteId(assetId); - if (asset == null) { - return; - } - - // Since we only have a single asset, we can just show GroupAssetBy.none - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.none); - - ref.read(currentAssetProvider.notifier).set(asset); - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - unawaited(context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList))); - } - - /// BOTTOM SHEET CALLBACKS - - Future onMapMoved() async { - assetsDebouncer.run(updateAssetsInBounds); - } - - void onBottomSheetScrolled(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (assetMarker != null) { - updateAssetMarkerPosition(assetMarker); - } - } - - void onZoomToAsset(String assetRemoteId) { - final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); - if (mapController.value != null && assetMarker != null) { - // Offset the latitude a little to show the marker just above the viewports center - final offset = context.isMobile ? 0.02 : 0; - final latlng = LatLng(assetMarker.latLng.latitude - offset, assetMarker.latLng.longitude); - mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onZoomToLocation() async { - final (location, error) = await MapUtils.checkPermAndGetLocation(context: context); - if (error != null) { - if (error == LocationPermission.unableToDetermine && context.mounted) { - ImmichToast.show( - context: context, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - msg: "map_cannot_get_user_location".tr(), - ); - } - return; - } - - if (mapController.value != null && location != null) { - await mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), - ); - } - } - - void onAssetsSelected(bool selected, Set selection) { - selectedAssets.value = selected ? selection : {}; - } - - return MapThemeOverride( - mapBuilder: (style) => context.isMobile - // Single-column - ? Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - // Should be a part of the body and not scaffold::bottomsheet for the - // location button to be hit testable - MapBottomSheet( - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - onZoomToLocation: onZoomToLocation, - selectedAssets: selectedAssets, - ), - ], - ), - ) - // Two-pane - : Row( - children: [ - Expanded( - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: MapAppBar(selectedAssets: selectedAssets), - body: Stack( - children: [ - _MapWithMarker( - initialLocation: initialLocation, - style: style, - selectedMarker: selectedMarker, - onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, - onMarkerTapped: onMarkerTapped, - ), - Positioned( - right: 0, - bottom: context.padding.bottom + 16, - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ], - ), - ), - ), - Expanded( - child: LayoutBuilder( - builder: (ctx, constraints) => MapAssetGrid( - controller: scrollController, - mapEventStream: bottomSheetStreamController.stream, - onGridAssetChanged: onBottomSheetScrolled, - onZoomToAsset: onZoomToAsset, - onAssetsSelected: onAssetsSelected, - selectedAssets: selectedAssets, - ), - ), - ), - ], - ), - ); - } -} - -class _AssetMarkerMeta { - final Point point; - final MapMarker marker; - final bool shouldAnimate; - - const _AssetMarkerMeta({required this.point, required this.marker, required this.shouldAnimate}); - - @override - String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; -} - -class _MapWithMarker extends StatelessWidget { - final AsyncValue style; - final MapCreatedCallback onMapCreated; - final OnCameraIdleCallback onMapMoved; - final OnMapClickCallback onMapClicked; - final OnStyleLoadedCallback onStyleLoaded; - final Function()? onMarkerTapped; - final ValueNotifier<_AssetMarkerMeta?> selectedMarker; - final LatLng? initialLocation; - - const _MapWithMarker({ - required this.style, - required this.onMapCreated, - required this.onMapMoved, - required this.onMapClicked, - required this.onStyleLoaded, - required this.selectedMarker, - this.onMarkerTapped, - this.initialLocation, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (ctx, constraints) => SizedBox( - height: constraints.maxHeight, - width: constraints.maxWidth, - child: Stack( - children: [ - style.widgetWhen( - onData: (style) => MapLibreMap( - attributionButtonMargins: const Point(8, kToolbarHeight), - initialCameraPosition: CameraPosition( - target: initialLocation ?? const LatLng(0, 0), - zoom: initialLocation != null ? 12 : 0, - ), - styleString: style, - // This is needed to update the selectedMarker's position on map camera updates - // The changes are notified through the mapController ValueListener which is added in [onMapCreated] - trackCameraPosition: true, - onMapCreated: onMapCreated, - onCameraIdle: onMapMoved, - onMapClick: onMapClicked, - onStyleLoadedCallback: onStyleLoaded, - tiltGesturesEnabled: false, - dragEnabled: false, - myLocationEnabled: false, - attributionButtonPosition: AttributionButtonPosition.topRight, - rotateGesturesEnabled: false, - ), - ), - ValueListenableBuilder( - valueListenable: selectedMarker, - builder: (ctx, value, _) => value != null - ? PositionedAssetMarkerIcon( - point: value.point, - assetRemoteId: value.marker.assetRemoteId, - assetThumbhash: '', - durationInMilliseconds: value.shouldAnimate ? 100 : 0, - onTap: onMarkerTapped, - ) - : const SizedBox.shrink(), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart deleted file mode 100644 index 8375eb14fd..0000000000 --- a/mobile/lib/pages/search/person_result.page.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -@RoutePage() -class PersonResultPage extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonResultPage({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final name = useState(personName); - - showEditNameDialog() { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: name.value); - }, - ).then((result) { - if (result != null && result.success) { - name.value = result.updatedName; - } - }); - } - - void buildBottomSheet() { - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - useSafeArea: true, - builder: (context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit_outlined), - title: const Text('edit_name', style: TextStyle(fontWeight: FontWeight.bold)).tr(), - onTap: showEditNameDialog, - ), - ], - ), - ); - }, - ); - } - - buildTitleBlock() { - return GestureDetector( - onTap: showEditNameDialog, - child: name.value.isEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('add_a_name', style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor)).tr(), - Text('find_them_fast', style: context.textTheme.labelLarge).tr(), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(name.value, style: context.textTheme.titleLarge, overflow: TextOverflow.ellipsis)], - ), - ); - } - - return Scaffold( - appBar: AppBar( - title: Text(name.value), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - actions: [IconButton(onPressed: buildBottomSheet, icon: const Icon(Icons.more_vert_rounded))], - ), - body: MultiselectGrid( - renderListProvider: personAssetsProvider(personId), - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar(radius: 36, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(personId))), - Expanded( - child: Padding(padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: buildTitleBlock()), - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/pages/search/recently_taken.page.dart b/mobile/lib/pages/search/recently_taken.page.dart deleted file mode 100644 index 988af2faf0..0000000000 --- a/mobile/lib/pages/search/recently_taken.page.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/recently_taken_asset.provider.dart'; - -@RoutePage() -class RecentlyTakenPage extends HookConsumerWidget { - const RecentlyTakenPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final recents = ref.watch(recentlyTakenAssetProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('recently_taken_page_title').tr(), - leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded)), - ), - body: recents.widgetWhen(onData: (searchResponse) => ImmichAssetGrid(assets: searchResponse)), - ); - } -} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart deleted file mode 100644 index dbd32ac94b..0000000000 --- a/mobile/lib/pages/search/search.page.dart +++ /dev/null @@ -1,760 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; -import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; -import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart'; -import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; -import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; - -@RoutePage() -class SearchPage extends HookConsumerWidget { - const SearchPage({super.key, this.prefilter}); - - final SearchFilter? prefilter; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final textSearchType = useState(TextSearchType.context); - final searchHintText = useState('sunrise_on_the_beach'.tr()); - final textSearchController = useTextEditingController(); - final filter = useState( - SearchFilter( - people: prefilter?.people ?? {}, - location: prefilter?.location ?? SearchLocationFilter(), - camera: prefilter?.camera ?? SearchCameraFilter(), - date: prefilter?.date ?? SearchDateFilter(), - display: prefilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - mediaType: prefilter?.mediaType ?? AssetType.other, - rating: prefilter?.rating ?? SearchRatingFilter(), - language: "${context.locale.languageCode}-${context.locale.countryCode}", - ), - ); - - final previousFilter = useState(null); - - final peopleCurrentFilterWidget = useState(null); - final dateRangeCurrentFilterWidget = useState(null); - final cameraCurrentFilterWidget = useState(null); - final locationCurrentFilterWidget = useState(null); - final mediaTypeCurrentFilterWidget = useState(null); - final displayOptionCurrentFilterWidget = useState(null); - - final isSearching = useState(false); - - SnackBar searchInfoSnackBar(String message) { - return SnackBar( - content: Text(message, style: context.textTheme.labelLarge), - showCloseIcon: true, - behavior: SnackBarBehavior.fixed, - closeIconColor: context.colorScheme.onSurface, - ); - } - - search() async { - if (filter.value.isEmpty) { - return; - } - - if (prefilter == null && filter.value == previousFilter.value) { - return; - } - - isSearching.value = true; - ref.watch(paginatedSearchProvider.notifier).clear(); - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_result'.tr())); - } - - previousFilter.value = filter.value; - isSearching.value = false; - } - - loadMoreSearchResult() async { - isSearching.value = true; - final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value); - - if (!hasResult) { - context.showSnackBar(searchInfoSnackBar('search_no_more_result'.tr())); - } - - isSearching.value = false; - } - - searchPrefilter() { - if (prefilter != null) { - Future.delayed(Duration.zero, () { - search(); - - if (prefilter!.location.city != null) { - locationCurrentFilterWidget.value = Text(prefilter!.location.city!, style: context.textTheme.labelLarge); - } - }); - } - } - - useEffect(() { - Future.microtask(() => ref.invalidate(paginatedSearchProvider)); - searchPrefilter(); - - return null; - }, []); - - showPeoplePicker() { - handleOnSelect(Set value) { - filter.value = filter.value.copyWith(people: value); - - peopleCurrentFilterWidget.value = Text( - value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(people: {}); - - peopleCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.tr(), - expanded: true, - onSearch: search, - onClear: handleClear, - child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), - ), - ), - ); - } - - showLocationPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - location: SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']), - ); - - final locationText = []; - if (value['country'] != null) { - locationText.add(value['country']!); - } - - if (value['state'] != null) { - locationText.add(value['state']!); - } - - if (value['city'] != null) { - locationText.add(value['city']!); - } - - locationCurrentFilterWidget.value = Text(locationText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith(location: SearchLocationFilter()); - - locationCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Container( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), - ), - ), - ), - ), - ); - } - - showCameraPicker() { - handleOnSelect(Map value) { - filter.value = filter.value.copyWith( - camera: SearchCameraFilter(make: value['make'], model: value['model']), - ); - - cameraCurrentFilterWidget.value = Text( - '${value['make'] ?? ''} ${value['model'] ?? ''}', - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(camera: SearchCameraFilter()); - - cameraCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.tr(), - onSearch: search, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), - ), - ), - ); - } - - showDatePicker() async { - final firstDate = DateTime(1900); - final lastDate = DateTime.now(); - - final date = await showDateRangePicker( - context: context, - firstDate: firstDate, - lastDate: lastDate, - currentDate: DateTime.now(), - initialDateRange: DateTimeRange( - start: filter.value.date.takenAfter ?? lastDate, - end: filter.value.date.takenBefore ?? lastDate, - ), - helpText: 'search_filter_date_title'.tr(), - cancelText: 'cancel'.tr(), - confirmText: 'select'.tr(), - saveText: 'save'.tr(), - errorFormatText: 'invalid_date_format'.tr(), - errorInvalidText: 'invalid_date'.tr(), - fieldStartHintText: 'start_date'.tr(), - fieldEndHintText: 'end_date'.tr(), - initialEntryMode: DatePickerEntryMode.calendar, - keyboardType: TextInputType.text, - ); - - if (date == null) { - filter.value = filter.value.copyWith(date: SearchDateFilter()); - - dateRangeCurrentFilterWidget.value = null; - unawaited(search()); - return; - } - - filter.value = filter.value.copyWith( - date: SearchDateFilter( - takenAfter: date.start, - takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)), - ), - ); - - // If date range is less than 24 hours, set the end date to the end of the day - if (date.end.difference(date.start).inHours < 24) { - dateRangeCurrentFilterWidget.value = Text( - DateFormat.yMMMd().format(date.start.toLocal()), - style: context.textTheme.labelLarge, - ); - } else { - dateRangeCurrentFilterWidget.value = Text( - 'search_filter_date_interval'.tr( - namedArgs: { - "start": DateFormat.yMMMd().format(date.start.toLocal()), - "end": DateFormat.yMMMd().format(date.end.toLocal()), - }, - ), - style: context.textTheme.labelLarge, - ); - } - - unawaited(search()); - } - - // MEDIA PICKER - showMediaTypePicker() { - handleOnSelected(AssetType assetType) { - filter.value = filter.value.copyWith(mediaType: assetType); - - mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image - ? 'image'.tr() - : assetType == AssetType.video - ? 'video'.tr() - : 'all'.tr(), - style: context.textTheme.labelLarge, - ); - } - - handleClear() { - filter.value = filter.value.copyWith(mediaType: AssetType.other); - - mediaTypeCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.tr(), - onSearch: search, - onClear: handleClear, - child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), - ), - ); - } - - // DISPLAY OPTION - showDisplayOptionPicker() { - handleOnSelect(Map value) { - final filterText = []; - value.forEach((key, value) { - switch (key) { - case DisplayOption.notInAlbum: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isNotInAlbum: value)); - if (value) { - filterText.add('search_filter_display_option_not_in_album'.tr()); - } - break; - case DisplayOption.archive: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isArchive: value)); - if (value) { - filterText.add('archive'.tr()); - } - break; - case DisplayOption.favorite: - filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isFavorite: value)); - if (value) { - filterText.add('favorite'.tr()); - } - break; - } - }); - - if (filterText.isEmpty) { - displayOptionCurrentFilterWidget.value = null; - return; - } - - displayOptionCurrentFilterWidget.value = Text(filterText.join(', '), style: context.textTheme.labelLarge); - } - - handleClear() { - filter.value = filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - ); - - displayOptionCurrentFilterWidget.value = null; - search(); - } - - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'display_options'.tr(), - onSearch: search, - onClear: handleClear, - child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), - ), - ); - } - - handleTextSubmitted(String value) { - switch (textSearchType.value) { - case TextSearchType.context: - filter.value = filter.value.copyWith(filename: '', context: value, description: '', ocr: ''); - - break; - case TextSearchType.filename: - filter.value = filter.value.copyWith(filename: value, context: '', description: '', ocr: ''); - - break; - case TextSearchType.description: - filter.value = filter.value.copyWith(filename: '', context: '', description: value, ocr: ''); - break; - case TextSearchType.ocr: - filter.value = filter.value.copyWith(filename: '', context: '', description: '', ocr: value); - break; - } - - search(); - } - - IconData getSearchPrefixIcon() => switch (textSearchType.value) { - TextSearchType.context => Icons.image_search_rounded, - TextSearchType.filename => Icons.abc_rounded, - TextSearchType.description => Icons.text_snippet_outlined, - TextSearchType.ocr => Icons.document_scanner_outlined, - }; - - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - automaticallyImplyLeading: true, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: MenuAnchor( - style: MenuStyle( - elevation: const WidgetStatePropertyAll(1), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - ), - padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), - ), - builder: (BuildContext context, MenuController controller, Widget? child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.more_vert_rounded), - tooltip: 'show_text_search_menu'.tr(), - ); - }, - menuChildren: [ - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.image_search_rounded), - title: Text( - 'search_by_context'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, - ), - onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.abc_rounded), - title: Text( - 'search_filter_filename'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.filename, - ), - onPressed: () { - textSearchType.value = TextSearchType.filename; - searchHintText.value = 'file_name_or_extension'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.text_snippet_outlined), - title: Text( - 'search_by_description'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.description, - ), - onPressed: () { - textSearchType.value = TextSearchType.description; - searchHintText.value = 'search_by_description_example'.tr(); - }, - ), - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.document_scanner_outlined), - title: Text( - 'search_filter_ocr'.tr(), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, - ), - ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.ocr, - ), - onPressed: () { - textSearchType.value = TextSearchType.ocr; - searchHintText.value = 'search_by_ocr_example'.tr(); - }, - ), - ], - ), - ), - ], - title: Container( - decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), - borderRadius: const BorderRadius.all(Radius.circular(24)), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withValues(alpha: 0.075), - context.colorScheme.primary.withValues(alpha: 0.09), - context.colorScheme.primary.withValues(alpha: 0.075), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: SearchField( - hintText: searchHintText.value, - key: const Key('search_text_field'), - controller: textSearchController, - contentPadding: prefilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), - prefixIcon: prefilter != null ? null : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary), - onSubmitted: handleTextSubmitted, - focusNode: ref.watch(searchInputFocusProvider), - ), - ), - ), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: SizedBox( - height: 50, - child: ListView( - key: const Key('search_filter_chip_list'), - shrinkWrap: true, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - children: [ - SearchFilterChip( - icon: Icons.people_alt_outlined, - onTap: showPeoplePicker, - label: 'people'.tr(), - currentFilter: peopleCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.location_on_outlined, - onTap: showLocationPicker, - label: 'search_filter_location'.tr(), - currentFilter: locationCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.camera_alt_outlined, - onTap: showCameraPicker, - label: 'camera'.tr(), - currentFilter: cameraCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.date_range_outlined, - onTap: showDatePicker, - label: 'search_filter_date'.tr(), - currentFilter: dateRangeCurrentFilterWidget.value, - ), - SearchFilterChip( - key: const Key('media_type_chip'), - icon: Icons.video_collection_outlined, - onTap: showMediaTypePicker, - label: 'search_filter_media_type'.tr(), - currentFilter: mediaTypeCurrentFilterWidget.value, - ), - SearchFilterChip( - icon: Icons.display_settings_outlined, - onTap: showDisplayOptionPicker, - label: 'search_filter_display_options'.tr(), - currentFilter: displayOptionCurrentFilterWidget.value, - ), - ], - ), - ), - ), - if (isSearching.value) - const Expanded(child: Center(child: CircularProgressIndicator())) - else - SearchResultGrid(onScrollEnd: loadMoreSearchResult, isSearching: isSearching.value), - ], - ), - ); - } -} - -class SearchResultGrid extends StatelessWidget { - final VoidCallback onScrollEnd; - final bool isSearching; - - const SearchResultGrid({super.key, required this.onScrollEnd, this.isSearching = false}); - - @override - Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: NotificationListener( - onNotification: (notification) { - final isBottomSheetNotification = - notification.context?.findAncestorWidgetOfExactType() != null; - - final metrics = notification.metrics; - final isVerticalScroll = metrics.axis == Axis.vertical; - - if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) { - onScrollEnd(); - } - - return true; - }, - child: MultiselectGrid( - renderListProvider: paginatedSearchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - dragScrollLabelEnabled: false, - emptyIndicator: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: !isSearching ? const SearchEmptyContent() : const SizedBox.shrink(), - ), - ), - ), - ), - ); - } -} - -class SearchEmptyContent extends StatelessWidget { - const SearchEmptyContent({super.key}); - - @override - Widget build(BuildContext context) { - return NotificationListener( - onNotification: (_) => true, - child: ListView( - shrinkWrap: false, - children: [ - const SizedBox(height: 40), - Center( - child: Image.asset( - context.isDarkTheme ? 'assets/polaroid-dark.png' : 'assets/polaroid-light.png', - height: 125, - ), - ), - const SizedBox(height: 16), - Center(child: Text('search_page_search_photos_videos'.tr(), style: context.textTheme.labelLarge)), - const SizedBox(height: 32), - const QuickLinkList(), - ], - ), - ); - } -} - -class QuickLinkList extends StatelessWidget { - const QuickLinkList({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(10), - context.colorScheme.primary.withAlpha(15), - context.colorScheme.primary.withAlpha(20), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - QuickLink( - title: 'recently_taken'.tr(), - icon: Icons.schedule_outlined, - isTop: true, - onTap: () => context.pushRoute(const RecentlyTakenRoute()), - ), - QuickLink( - title: 'videos'.tr(), - icon: Icons.play_circle_outline_rounded, - onTap: () => context.pushRoute(const AllVideosRoute()), - ), - QuickLink( - title: 'favorites'.tr(), - icon: Icons.favorite_border_rounded, - isBottom: true, - onTap: () => context.pushRoute(const FavoritesRoute()), - ), - ], - ), - ); - } -} - -class QuickLink extends StatelessWidget { - final String title; - final IconData icon; - final VoidCallback onTap; - final bool isTop; - final bool isBottom; - - const QuickLink({ - super.key, - required this.title, - required this.icon, - required this.onTap, - this.isTop = false, - this.isBottom = false, - }); - - @override - Widget build(BuildContext context) { - final borderRadius = BorderRadius.only( - topLeft: Radius.circular(isTop ? 20 : 0), - topRight: Radius.circular(isTop ? 20 : 0), - bottomLeft: Radius.circular(isBottom ? 20 : 0), - bottomRight: Radius.circular(isBottom ? 20 : 0), - ); - - return ListTile( - shape: RoundedRectangleBorder(borderRadius: borderRadius), - leading: Icon(icon, size: 26), - title: Text(title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), - onTap: onTap, - ); - } -} diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 2be51fbfc9..2744b187de 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -66,7 +65,7 @@ class ShareIntentPage extends ConsumerWidget { ), leading: IconButton( onPressed: () { - context.navigateTo(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); + context.navigateTo(const TabShellRoute()); }, icon: const Icon(Icons.arrow_back), ), diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index d34ce3e776..32bbd7e60b 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -89,7 +89,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState Function(List edits) applyEdits; + + const DriftEditImagePage({super.key, required this.image, required this.applyEdits}); + + @override + ConsumerState createState() => _DriftEditImagePageState(); +} + +class _DriftEditImagePageState extends ConsumerState with TickerProviderStateMixin { + Future _saveEditedImage() async { + ref.read(editorStateProvider.notifier).setIsEditing(true); + + final editorState = ref.read(editorStateProvider); + final cropParameters = convertRectToCropParameters( + editorState.crop, + editorState.originalWidth, + editorState.originalHeight, + ); + final edits = []; + + if (cropParameters.width != editorState.originalWidth || cropParameters.height != editorState.originalHeight) { + edits.add(CropEdit(cropParameters)); + } + + if (editorState.flipHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (editorState.flipVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + final normalizedRotation = (editorState.rotationAngle % 360 + 360) % 360; + if (normalizedRotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: normalizedRotation))); + } + + try { + await widget.applyEdits(edits); + ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); + Navigator.of(context).pop(); + } catch (e) { + ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); + } finally { + ref.read(editorStateProvider.notifier).setIsEditing(false); + } + } + + Future _showDiscardChangesDialog() { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('editor_discard_edits_title'.tr()), + content: Text('editor_discard_edits_prompt'.tr()), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), + ), + child: Text('cancel'.tr()), + ), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return PopScope( + canPop: !hasUnsavedEdits, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + final shouldDiscard = await _showDiscardChangesDialog() ?? false; + if (shouldDiscard && mounted) { + Navigator.of(context).pop(); + } + }, + child: Theme( + data: getThemeData(colorScheme: ref.watch(immichThemeProvider).dark, locale: context.locale), + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + title: Text("edit".tr()), + leading: ImmichCloseButton(onPressed: () => Navigator.of(context).maybePop()), + actions: [_SaveEditsButton(onSave: _saveEditedImage)], + ), + backgroundColor: Colors.black, + body: SafeArea( + bottom: false, + child: Column( + children: [ + Expanded(child: _EditorPreview(image: widget.image)), + AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: ref.watch(immichThemeProvider).dark.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TransformControls(), + Padding( + padding: EdgeInsets.only(bottom: 36, left: 24, right: 24), + child: Row(children: [Spacer(), _ResetEditsButton()]), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _AspectRatioButton extends StatelessWidget { + final AspectRatioPreset ratio; + final bool isSelected; + final VoidCallback onPressed; + + const _AspectRatioButton({required this.ratio, required this.isSelected, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + iconSize: 36, + icon: Transform.rotate( + angle: ratio.iconRotated ? pi / 2 : 0, + child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color), + ), + onPressed: onPressed, + ), + Text(ratio.label, style: context.textTheme.displayMedium), + ], + ); + } +} + +class _AspectRatioSelector extends ConsumerWidget { + const _AspectRatioSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + // the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees + double? selectedAspectRatio = editorState.aspectRatio; + if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) { + selectedAspectRatio = 1 / selectedAspectRatio; + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: AspectRatioPreset.values.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _AspectRatioButton( + ratio: entry, + isSelected: selectedAspectRatio == entry.ratio, + onPressed: () => editorNotifier.setAspectRatio(entry.ratio), + ), + ); + }).toList(), + ), + ); + } +} + +class _TransformControls extends ConsumerWidget { + const _TransformControls(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorNotifier = ref.read(editorStateProvider.notifier); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 20, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ImmichIconButton( + icon: Icons.rotate_left, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCCW, + ), + const SizedBox(width: 8), + ImmichIconButton( + icon: Icons.rotate_right, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.rotateCW, + ), + ], + ), + Row( + children: [ + ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipHorizontally, + ), + const SizedBox(width: 8), + Transform.rotate( + angle: pi / 2, + child: ImmichIconButton( + icon: Icons.flip, + variant: ImmichVariant.ghost, + color: ImmichColor.secondary, + onPressed: editorNotifier.flipVertically, + ), + ), + ], + ), + ], + ), + ), + const _AspectRatioSelector(), + const SizedBox(height: 32), + ], + ); + } +} + +class _SaveEditsButton extends ConsumerWidget { + final VoidCallback onSave; + + const _SaveEditsButton({required this.onSave}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isApplyingEdits = ref.watch(editorStateProvider.select((state) => state.isApplyingEdits)); + final hasUnsavedEdits = ref.watch(editorStateProvider.select((state) => state.hasUnsavedEdits)); + + return isApplyingEdits + ? const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2.5)), + ) + : ImmichIconButton( + icon: Icons.done_rounded, + color: ImmichColor.primary, + variant: ImmichVariant.ghost, + disabled: !hasUnsavedEdits, + onPressed: onSave, + ); + } +} + +class _ResetEditsButton extends ConsumerWidget { + const _ResetEditsButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + return ImmichTextButton( + labelText: 'reset'.tr(), + onPressed: editorNotifier.resetEdits, + variant: ImmichVariant.ghost, + expanded: false, + disabled: !editorState.hasEdits || editorState.isApplyingEdits, + ); + } +} + +class _EditorPreview extends ConsumerStatefulWidget { + final Image image; + + const _EditorPreview({required this.image}); + + @override + ConsumerState<_EditorPreview> createState() => _EditorPreviewState(); +} + +class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProviderStateMixin { + late final CropController cropController; + + @override + void initState() { + super.initState(); + + cropController = CropController(); + cropController.crop = ref.read(editorStateProvider.select((state) => state.crop)); + cropController.addListener(onCrop); + } + + void onCrop() { + if (!mounted || cropController.crop == ref.read(editorStateProvider).crop) { + return; + } + + ref.read(editorStateProvider.notifier).setCrop(cropController.crop); + } + + @override + void dispose() { + cropController.removeListener(onCrop); + cropController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final editorState = ref.watch(editorStateProvider); + final editorNotifier = ref.read(editorStateProvider.notifier); + + ref.listen(editorStateProvider, (_, current) { + cropController.aspectRatio = current.aspectRatio; + + if (cropController.crop != current.crop) { + cropController.crop = current.crop; + } + }); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // Calculate the bounding box size needed for the rotated container + final baseWidth = constraints.maxWidth * 0.9; + final baseHeight = constraints.maxHeight * 0.95; + + return Center( + child: AnimatedRotation( + turns: editorState.rotationAngle / 360, + duration: editorState.animationDuration, + curve: Curves.easeInOut, + onEnd: editorNotifier.normalizeRotation, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..scaleByDouble( + editorState.flipHorizontal ? -1.0 : 1.0, + editorState.flipVertical ? -1.0 : 1.0, + 1.0, + 1.0, + ), + child: Container( + padding: const EdgeInsets.all(10), + width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight, + height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth, + child: CropImage(controller: cropController, image: widget.image, gridColor: Colors.white), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/pages/edit/editor.provider.dart b/mobile/lib/presentation/pages/edit/editor.provider.dart new file mode 100644 index 0000000000..21b5268912 --- /dev/null +++ b/mobile/lib/presentation/pages/edit/editor.provider.dart @@ -0,0 +1,210 @@ +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; + +final editorStateProvider = NotifierProvider(EditorProvider.new); + +class EditorProvider extends Notifier { + @override + EditorState build() { + return const EditorState(); + } + + void clear() { + state = const EditorState(); + } + + void init(List edits, ExifInfo exifInfo) { + clear(); + + final existingCrop = edits.whereType().firstOrNull; + + final originalWidth = exifInfo.isFlipped ? exifInfo.height : exifInfo.width; + final originalHeight = exifInfo.isFlipped ? exifInfo.width : exifInfo.height; + + Rect crop = existingCrop != null && originalWidth != null && originalHeight != null + ? convertCropParametersToRect(existingCrop.parameters, originalWidth, originalHeight) + : const Rect.fromLTRB(0, 0, 1, 1); + + final transform = normalizeTransformEdits(edits); + + state = state.copyWith( + originalWidth: originalWidth, + originalHeight: originalHeight, + crop: crop, + flipHorizontal: transform.mirrorHorizontal, + flipVertical: transform.mirrorVertical, + ); + + _animateRotation(transform.rotation.toInt(), duration: Duration.zero); + } + + void _animateRotation(int angle, {Duration duration = const Duration(milliseconds: 300)}) { + state = state.copyWith(rotationAngle: angle, animationDuration: duration); + } + + void normalizeRotation() { + final normalizedAngle = ((state.rotationAngle % 360) + 360) % 360; + if (normalizedAngle != state.rotationAngle) { + state = state.copyWith(rotationAngle: normalizedAngle, animationDuration: Duration.zero); + } + } + + void setIsEditing(bool isApplyingEdits) { + state = state.copyWith(isApplyingEdits: isApplyingEdits); + } + + void setCrop(Rect crop) { + state = state.copyWith(crop: crop, hasUnsavedEdits: true); + } + + void setAspectRatio(double? aspectRatio) { + if (aspectRatio != null && state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations + aspectRatio = 1 / aspectRatio; + } + + state = state.copyWith(aspectRatio: aspectRatio); + } + + void resetEdits() { + _animateRotation(0); + + state = state.copyWith( + flipHorizontal: false, + flipVertical: false, + crop: const Rect.fromLTRB(0, 0, 1, 1), + aspectRatio: null, + hasUnsavedEdits: true, + ); + } + + void rotateCCW() { + _animateRotation(state.rotationAngle - 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void rotateCW() { + _animateRotation(state.rotationAngle + 90); + state = state.copyWith(hasUnsavedEdits: true); + } + + void flipHorizontally() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping horizontally is equivalent to flipping vertically + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } + } + + void flipVertically() { + if (state.rotationAngle % 180 != 0) { + // When rotated 90 or 270 degrees, flipping vertically is equivalent to flipping horizontally + state = state.copyWith(flipHorizontal: !state.flipHorizontal, hasUnsavedEdits: true); + } else { + state = state.copyWith(flipVertical: !state.flipVertical, hasUnsavedEdits: true); + } + } +} + +class EditorState { + final bool isApplyingEdits; + + final int rotationAngle; + final bool flipHorizontal; + final bool flipVertical; + final Rect crop; + final double? aspectRatio; + + final int originalWidth; + final int originalHeight; + + final Duration animationDuration; + + final bool hasUnsavedEdits; + + const EditorState({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + Rect? crop, + this.aspectRatio, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + bool? hasUnsavedEdits, + }) : isApplyingEdits = isApplyingEdits ?? false, + rotationAngle = rotationAngle ?? 0, + flipHorizontal = flipHorizontal ?? false, + flipVertical = flipVertical ?? false, + animationDuration = animationDuration ?? Duration.zero, + originalWidth = originalWidth ?? 0, + originalHeight = originalHeight ?? 0, + crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1), + hasUnsavedEdits = hasUnsavedEdits ?? false; + + EditorState copyWith({ + bool? isApplyingEdits, + int? rotationAngle, + bool? flipHorizontal, + bool? flipVertical, + double? aspectRatio = double.infinity, + int? originalWidth, + int? originalHeight, + Duration? animationDuration, + Rect? crop, + bool? hasUnsavedEdits, + }) { + return EditorState( + isApplyingEdits: isApplyingEdits ?? this.isApplyingEdits, + rotationAngle: rotationAngle ?? this.rotationAngle, + flipHorizontal: flipHorizontal ?? this.flipHorizontal, + flipVertical: flipVertical ?? this.flipVertical, + aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio, + animationDuration: animationDuration ?? this.animationDuration, + originalWidth: originalWidth ?? this.originalWidth, + originalHeight: originalHeight ?? this.originalHeight, + crop: crop ?? this.crop, + hasUnsavedEdits: hasUnsavedEdits ?? this.hasUnsavedEdits, + ); + } + + bool get hasEdits { + return rotationAngle != 0 || flipHorizontal || flipVertical || crop != const Rect.fromLTRB(0, 0, 1, 1); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is EditorState && + other.isApplyingEdits == isApplyingEdits && + other.rotationAngle == rotationAngle && + other.flipHorizontal == flipHorizontal && + other.flipVertical == flipVertical && + other.crop == crop && + other.aspectRatio == aspectRatio && + other.originalWidth == originalWidth && + other.originalHeight == originalHeight && + other.animationDuration == animationDuration && + other.hasUnsavedEdits == hasUnsavedEdits; + } + + @override + int get hashCode { + return isApplyingEdits.hashCode ^ + rotationAngle.hashCode ^ + flipHorizontal.hashCode ^ + flipVertical.hashCode ^ + crop.hashCode ^ + aspectRatio.hashCode ^ + originalWidth.hashCode ^ + originalHeight.hashCode ^ + animationDuration.hashCode ^ + hasUnsavedEdits.hashCode; + } +} diff --git a/mobile/lib/presentation/pages/editing/drift_crop.page.dart b/mobile/lib/presentation/pages/editing/drift_crop.page.dart deleted file mode 100644 index a213e4c640..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_crop.page.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; -import 'package:immich_ui/immich_ui.dart'; - -/// A widget for cropping an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to crop an image and then navigate to the [EditImagePage] with the -/// cropped image. - -@RoutePage() -class DriftCropImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - const DriftCropImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final cropController = useCropController(); - final aspectRatio = useState(null); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("crop".tr()), - leading: const ImmichCloseButton(), - actions: [ - ImmichIconButton( - icon: Icons.done_rounded, - color: ImmichColor.primary, - variant: ImmichVariant.ghost, - onPressed: () async { - final croppedImage = await cropController.croppedImage(); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage(controller: cropController, image: image, gridColor: Colors.white), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ImmichIconButton( - icon: Icons.rotate_left, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateLeft(), - ), - ImmichIconButton( - icon: Icons.rotate_right, - variant: ImmichVariant.ghost, - color: ImmichColor.secondary, - onPressed: () => cropController.rotateRight(), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _AspectRatioButton extends StatelessWidget { - final CropController cropController; - final ValueNotifier aspectRatio; - final double? ratio; - final String label; - - const _AspectRatioButton({ - required this.cropController, - required this.aspectRatio, - required this.ratio, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(switch (label) { - 'Free' => Icons.crop_free_rounded, - '1:1' => Icons.crop_square_rounded, - '16:9' => Icons.crop_16_9_rounded, - '3:2' => Icons.crop_3_2_rounded, - '7:5' => Icons.crop_7_5_rounded, - _ => Icons.crop_free_rounded, - }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), - onPressed: () { - cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); - aspectRatio.value = ratio; - cropController.aspectRatio = ratio; - }, - ), - Text(label, style: context.textTheme.displayMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_edit.page.dart b/mobile/lib/presentation/pages/editing/drift_edit.page.dart deleted file mode 100644 index 6d4ea4d3a6..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_edit.page.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/utils/image_converter.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as p; - -/// A stateless widget that provides functionality for editing an image. -/// -/// This widget allows users to edit an image provided either as an [Asset] or -/// directly as an [Image]. It ensures that exactly one of these is provided. -/// -/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone -/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. -@immutable -@RoutePage() -class DriftEditImagePage extends ConsumerWidget { - final BaseAsset asset; - final Image image; - final bool isEdited; - - const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - - void _exitEditing(BuildContext context) { - // this assumes that the only way to get to this page is from the AssetViewerRoute - context.navigator.popUntil((route) => route.data?.name == AssetViewerRoute.name); - } - - Future _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async { - try { - final Uint8List imageData = await imageToUint8List(image); - LocalAsset? localAsset; - - try { - localAsset = await ref - .read(fileMediaRepositoryProvider) - .saveLocalAsset(imageData, title: "${p.withoutExtension(asset.name)}_edited.jpg"); - } on PlatformException catch (e) { - // OS might not return the saved image back, so we handle that gracefully - // This can happen if app does not have full library access - Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e); - } - - unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true)); - _exitEditing(context); - ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!'); - - if (localAsset == null) { - return; - } - - await ref.read(foregroundUploadServiceProvider).uploadManual([localAsset]); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: Text("edit".tr()), - backgroundColor: context.scaffoldBackgroundColor, - leading: IconButton( - icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), - onPressed: () => _exitEditing(context), - ), - actions: [ - TextButton( - onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, - child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(7)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - spreadRadius: 2, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: Image(image: image.image, fit: BoxFit.contain), - ), - ), - ), - ), - bottomNavigationBar: Container( - height: 70, - margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(30)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftCropImageRoute(asset: asset, image: image)); - }, - ), - Text("crop".tr(), style: context.textTheme.displayMedium), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), - onPressed: () { - context.pushRoute(DriftFilterImageRoute(asset: asset, image: image)); - }, - ), - Text("filter".tr(), style: context.textTheme.displayMedium), - ], - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/presentation/pages/editing/drift_filter.page.dart b/mobile/lib/presentation/pages/editing/drift_filter.page.dart deleted file mode 100644 index 8198a41bbe..0000000000 --- a/mobile/lib/presentation/pages/editing/drift_filter.page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/constants/filters.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// A widget for filtering an image. -/// This widget uses [HookWidget] to manage its lifecycle and state. It allows -/// users to add filters to an image and then navigate to the [EditImagePage] with the -/// final composition.' -@RoutePage() -class DriftFilterImagePage extends HookWidget { - final Image image; - final BaseAsset asset; - - const DriftFilterImagePage({super.key, required this.image, required this.asset}); - - @override - Widget build(BuildContext context) { - final colorFilter = useState(filters[0]); - final selectedFilterIndex = useState(0); - - Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { - final completer = Completer(); - final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - - final paint = Paint()..colorFilter = filter; - canvas.drawImage(inputImage, Offset.zero, paint); - - recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { - completer.complete(image); - }); - - return completer.future; - } - - void applyFilter(ColorFilter filter, int index) { - colorFilter.value = filter; - selectedFilterIndex.value = index; - } - - Future applyFilterAndConvert(ColorFilter filter) async { - final completer = Completer(); - image.image - .resolve(ImageConfiguration.empty) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - completer.complete(info.image); - }), - ); - final uiImage = await completer.future; - - final filteredUiImage = await createFilteredImage(uiImage, filter); - final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - return Image.memory(pngBytes, fit: BoxFit.contain); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: context.scaffoldBackgroundColor, - title: Text("filter".tr()), - leading: CloseButton(color: context.primaryColor), - actions: [ - IconButton( - icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), - onPressed: () async { - final filteredImage = await applyFilterAndConvert(colorFilter.value); - unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true))); - }, - ), - ], - ), - backgroundColor: context.scaffoldBackgroundColor, - body: Column( - children: [ - SizedBox( - height: context.height * 0.7, - child: Center( - child: ColorFiltered(colorFilter: colorFilter.value, child: image), - ), - ), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: filters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _FilterButton( - image: image, - label: filterNames[index], - filter: filters[index], - isSelected: selectedFilterIndex.value == index, - onTap: () => applyFilter(filters[index], index), - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _FilterButton extends StatelessWidget { - final Image image; - final String label; - final ColorFilter filter; - final bool isSelected; - final VoidCallback onTap; - - const _FilterButton({ - required this.image, - required this.label, - required this.filter, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - GestureDetector( - onTap: onTap, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: ColorFiltered( - colorFilter: filter, - child: FittedBox(fit: BoxFit.cover, child: image), - ), - ), - ), - ), - const SizedBox(height: 10), - Text(label, style: context.themeData.textTheme.bodyMedium), - ], - ); - } -} diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 7e47a742ae..3ba4cf3497 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart index cad74ce658..564b02d884 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -1,10 +1,17 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class EditImageActionButton extends ConsumerWidget { @@ -14,13 +21,33 @@ class EditImageActionButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); - onPress() { - if (currentAsset == null) { + Future editImage(List edits) async { + if (currentAsset == null || currentAsset.remoteId == null) { return; } - final image = Image(image: getFullImageProvider(currentAsset)); - context.pushRoute(DriftEditImageRoute(asset: currentAsset, image: image, isEdited: false)); + await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits); + } + + Future onPress() async { + if (currentAsset == null || currentAsset.remoteId == null) { + return; + } + + final imageProvider = getFullImageProvider(currentAsset, edited: false); + + final image = Image(image: imageProvider); + final (edits, exifInfo) = await ( + ref.read(remoteAssetRepositoryProvider).getAssetEdits(currentAsset.remoteId!), + ref.read(remoteAssetRepositoryProvider).getExif(currentAsset.remoteId!), + ).wait; + + if (exifInfo == null) { + return; + } + + ref.read(editorStateProvider.notifier).init(edits, exifInfo); + await context.pushRoute(DriftEditImageRoute(image: image, applyEdits: editImage)); } return BaseActionButton( diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index bb42140d0a..0acbbce613 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 0c039847a4..e5b4607619 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -17,9 +17,9 @@ import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.wi import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index b51960bb05..cf7ffbd234 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -3,16 +3,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; class ViewerBottomBar extends ConsumerWidget { @@ -30,6 +32,7 @@ class ViewerBottomBar extends ConsumerWidget { final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final isInLockedView = ref.watch(inLockedViewProvider); + final serverInfo = ref.watch(serverInfoProvider); final originalTheme = context.themeData; @@ -38,7 +41,9 @@ class ViewerBottomBar extends ConsumerWidget { if (!isInLockedView) ...[ if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - if (asset.type == AssetType.image) const EditImageActionButton(), + // edit sync was added in 2.6.0 + if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) + const EditImageActionButton(), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), if (isOwner) ...[ diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 47ebd37014..ea416d9d71 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -147,7 +147,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide } } -ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) { +ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920), bool edited = true}) { // Create new provider and cache it final ImageProvider provider; if (_shouldUseLocalAsset(asset)) { @@ -170,13 +170,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 thumbhash: thumbhash, assetType: asset.type, isAnimated: asset.isAnimatedImage, + edited: edited, ); } return provider; } -ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution}) { +ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) { if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; return LocalThumbProvider(id: id, size: size, assetType: asset.type); @@ -184,7 +185,7 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; - return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash) : null; + return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited) : null; } bool _shouldUseLocalAsset(BaseAsset asset) => diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index d9cc053ccf..f7fc5868c3 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -13,11 +13,12 @@ import 'package:openapi/api.dart'; class RemoteImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String url; + final bool edited; - RemoteImageProvider({required this.url}); + RemoteImageProvider({required this.url, this.edited = true}); - RemoteImageProvider.thumbnail({required String assetId, required String thumbhash}) - : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash); + RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true}) + : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); @override Future obtainKey(ImageConfiguration configuration) { @@ -45,13 +46,13 @@ class RemoteImageProvider extends CancellableImageProvider bool operator ==(Object other) { if (identical(this, other)) return true; if (other is RemoteImageProvider) { - return url == other.url; + return url == other.url && edited == other.edited; } return false; } @override - int get hashCode => url.hashCode; + int get hashCode => url.hashCode ^ edited.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider @@ -60,12 +61,14 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -109,7 +114,12 @@ class RemoteFullImageProvider extends CancellableImageProvider assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode ^ isAnimated.hashCode ^ edited.hashCode; } diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 72f4e8bda6..3f406dd551 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -132,7 +132,7 @@ class _DriftMapState extends ConsumerState { // If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed, // which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever"). final currentRoute = ref.read(currentRouteNameProvider); - if (currentRoute == AssetViewerRoute.name || currentRoute == GalleryViewerRoute.name) { + if (currentRoute == AssetViewerRoute.name) { return; } diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart deleted file mode 100644 index 35634d77c8..0000000000 --- a/mobile/lib/providers/album/album.provider.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -final isRefreshingRemoteAlbumProvider = StateProvider((ref) => false); - -class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this.albumService, this.ref) : super([]) { - albumService.getAllRemoteAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchRemoteAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - final Ref ref; - late final StreamSubscription> _streamSub; - - Future refreshRemoteAlbums() async { - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true; - await albumService.refreshRemoteAlbums(); - ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false; - } - - Future refreshDeviceAlbums() => albumService.refreshDeviceAlbums(); - - Future deleteAlbum(Album album) => albumService.deleteAlbum(album); - - Future createAlbum(String albumTitle, Set assets) => albumService.createAlbum(albumTitle, assets, []); - - Future getAlbumByName(String albumName, {bool? remote, bool? shared, bool? owner}) => - albumService.getAlbumByName(albumName, remote: remote, shared: shared, owner: owner); - - /// Create an album on the server with the same name as the selected album for backup - /// First this will check if the album already exists on the server with name - /// If it does not exist, it will create the album on the server - Future createSyncAlbum(String albumName) async { - final album = await getAlbumByName(albumName, remote: true, owner: true); - if (album != null) { - return; - } - - await createAlbum(albumName, {}); - } - - Future leaveAlbum(Album album) async { - var res = await albumService.leaveAlbum(album); - - if (res) { - await deleteAlbum(album); - return true; - } else { - return false; - } - } - - void searchAlbums(String searchTerm, QuickFilterMode filterMode) async { - state = await albumService.search(searchTerm, filterMode); - } - - Future addUsers(Album album, List userIds) async { - await albumService.addUsers(album, userIds); - } - - Future removeUser(Album album, UserDto user) async { - final isRemoved = await albumService.removeUser(album, user); - - if (isRemoved && album.sharedUsers.isEmpty) { - state = state.where((element) => element.id != album.id).toList(); - } - - return isRemoved; - } - - Future addAssets(Album album, Iterable assets) async { - await albumService.addAssets(album, assets); - } - - Future removeAsset(Album album, Iterable assets) async { - return await albumService.removeAsset(album, assets); - } - - Future setActivitystatus(Album album, bool enabled) { - return albumService.setActivityStatus(album, enabled); - } - - Future toggleSortOrder(Album album) { - final order = album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - - return albumService.updateSortOrder(album, order); - } - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final albumProvider = StateNotifierProvider.autoDispose>((ref) { - return AlbumNotifier(ref.watch(albumServiceProvider), ref); -}); - -final albumWatcher = StreamProvider.autoDispose.family((ref, id) async* { - final albumService = ref.watch(albumServiceProvider); - - final album = await albumService.getAlbumById(id); - if (album != null) { - yield album; - } - - await for (final album in albumService.watchAlbum(id)) { - if (album != null) { - yield album; - } - } -}); - -class LocalAlbumsNotifier extends StateNotifier> { - LocalAlbumsNotifier(this.albumService) : super([]) { - albumService.getAllLocalAlbums().then((value) { - if (mounted) { - state = value; - } - }); - - _streamSub = albumService.watchLocalAlbums().listen((data) => state = data); - } - - final AlbumService albumService; - late final StreamSubscription> _streamSub; - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final localAlbumsProvider = StateNotifierProvider.autoDispose>((ref) { - return LocalAlbumsNotifier(ref.watch(albumServiceProvider)); -}); diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index c969dbd37d..ec4ae71d03 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,119 +1,19 @@ -import 'package:collection/collection.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'album_sort_by_options.provider.g.dart'; - -typedef AlbumSortFn = List Function(List albums, bool isReverse); - -class _AlbumSortHandlers { - const _AlbumSortHandlers._(); - - static const AlbumSortFn created = _sortByCreated; - static List _sortByCreated(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.createdAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn title = _sortByTitle; - static List _sortByTitle(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.name); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn lastModified = _sortByLastModified; - static List _sortByLastModified(List albums, bool isReverse) { - final sorted = albums.sortedBy((album) => album.modifiedAt); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn assetCount = _sortByAssetCount; - static List _sortByAssetCount(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostRecent = _sortByMostRecent; - static List _sortByMostRecent(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.endDate == null && b.endDate == null) { - return 0; - } - - if (a.endDate == null) { - // Put nulls at the end for recent sorting - return 1; - } - - if (b.endDate == null) { - return -1; - } - - // Sort by descending recent date - return b.endDate!.compareTo(a.endDate!); - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } - - static const AlbumSortFn mostOldest = _sortByMostOldest; - static List _sortByMostOldest(List albums, bool isReverse) { - final sorted = albums.sorted((a, b) { - if (a.startDate != null && b.startDate != null) { - return a.startDate!.compareTo(b.startDate!); - } - if (a.startDate == null) return 1; - if (b.startDate == null) return -1; - return 0; - }); - return (isReverse ? sorted.reversed : sorted).toList(); - } -} // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc), - assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc), - lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc), - created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc), - mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc), - mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc); + title(1, "library_page_sort_title", SortOrder.asc), + assetCount(4, "library_page_sort_asset_count", SortOrder.desc), + lastModified(3, "library_page_sort_last_modified", SortOrder.desc), + created(0, "library_page_sort_created", SortOrder.desc), + mostRecent(2, "sort_recent", SortOrder.desc), + mostOldest(5, "sort_oldest", SortOrder.asc); final int storeIndex; final String label; - final AlbumSortFn sortFn; final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder); + const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder); SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } - -@riverpod -class AlbumSortByOptions extends _$AlbumSortByOptions { - @override - AlbumSortMode build() { - final sortOpt = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortOrder); - return AlbumSortMode.values.firstWhere((e) => e.storeIndex == sortOpt, orElse: () => AlbumSortMode.title); - } - - void changeSortMode(AlbumSortMode sortOption) { - state = sortOption; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortOrder, sortOption.storeIndex); - } -} - -@riverpod -class AlbumSortOrder extends _$AlbumSortOrder { - @override - bool build() { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.selectedAlbumSortReverse); - } - - void changeSortDirection(bool isReverse) { - state = isReverse; - ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.selectedAlbumSortReverse, isReverse); - } -} diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart b/mobile/lib/providers/album/album_sort_by_options.provider.g.dart deleted file mode 100644 index 750329c9d5..0000000000 --- a/mobile/lib/providers/album/album_sort_by_options.provider.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'album_sort_by_options.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$albumSortByOptionsHash() => - r'dd8da5e730af555de1b86c3b157b6c93183523ac'; - -/// See also [AlbumSortByOptions]. -@ProviderFor(AlbumSortByOptions) -final albumSortByOptionsProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortByOptions.new, - name: r'albumSortByOptionsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortByOptionsHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortByOptions = AutoDisposeNotifier; -String _$albumSortOrderHash() => r'573dea45b4519e69386fc7104c72522e35713440'; - -/// See also [AlbumSortOrder]. -@ProviderFor(AlbumSortOrder) -final albumSortOrderProvider = - AutoDisposeNotifierProvider.internal( - AlbumSortOrder.new, - name: r'albumSortOrderProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$albumSortOrderHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$AlbumSortOrder = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart deleted file mode 100644 index f4ce047464..0000000000 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; - -class AlbumViewerNotifier extends StateNotifier { - AlbumViewerNotifier(this.ref) - : super(const AlbumViewerPageState(editTitleText: "", isEditAlbum: false, editDescriptionText: "")); - - final Ref ref; - - void enableEditAlbum() { - state = state.copyWith(isEditAlbum: true); - } - - void disableEditAlbum() { - state = state.copyWith(isEditAlbum: false); - } - - void setEditTitleText(String newTitle) { - state = state.copyWith(editTitleText: newTitle); - } - - void setEditDescriptionText(String newDescription) { - state = state.copyWith(editDescriptionText: newDescription); - } - - void remoteEditTitleText() { - state = state.copyWith(editTitleText: ""); - } - - void remoteEditDescriptionText() { - state = state.copyWith(editDescriptionText: ""); - } - - void resetState() { - state = state.copyWith(editTitleText: "", isEditAlbum: false, editDescriptionText: ""); - } - - Future changeAlbumTitle(Album album, String newAlbumTitle) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle); - - if (isSuccess) { - state = state.copyWith(editTitleText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editTitleText: "", isEditAlbum: false); - return false; - } - - Future changeAlbumDescription(Album album, String newAlbumDescription) async { - AlbumService service = ref.watch(albumServiceProvider); - - bool isSuccess = await service.changeDescriptionAlbum(album, newAlbumDescription); - - if (isSuccess) { - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return true; - } - - state = state.copyWith(editDescriptionText: "", isEditAlbum: false); - - return false; - } -} - -final albumViewerProvider = StateNotifierProvider((ref) { - return AlbumViewerNotifier(ref); -}); diff --git a/mobile/lib/providers/album/current_album.provider.dart b/mobile/lib/providers/album/current_album.provider.dart deleted file mode 100644 index bd22c7a7cd..0000000000 --- a/mobile/lib/providers/album/current_album.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_album.provider.g.dart'; - -@riverpod -class CurrentAlbum extends _$CurrentAlbum { - @override - Album? build() => null; - - void set(Album? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAlbumInternal extends _$CurrentAlbum {} diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart deleted file mode 100644 index 51146748c7..0000000000 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; - -final otherUsersProvider = FutureProvider.autoDispose>((ref) async { - UserService userService = ref.watch(userServiceProvider); - final currentUser = ref.watch(currentUserProvider); - - final allUsers = await userService.getAll(); - allUsers.removeWhere((u) => currentUser?.id == u.id); - return allUsers; -}); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 68007f283a..a5f67215a8 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -5,28 +5,17 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden } @@ -87,43 +76,15 @@ class AppLifeCycleNotifier extends StateNotifier { final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint(); _log.info("Using server URL: $endpoint"); - if (!Store.isBetaTimelineEnabled) { - final permission = _ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { - await _ref.read(backupProvider.notifier).resumeBackup(); - await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - } - } - await _ref.read(serverInfoProvider.notifier).getServerVersion(); } - if (!Store.isBetaTimelineEnabled) { - switch (_ref.read(tabProvider)) { - case TabEnum.home: - await _ref.read(assetProvider.notifier).getAllAsset(); - - case TabEnum.albums: - await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - case TabEnum.library: - case TabEnum.search: - break; - } - } else { - _ref.read(websocketProvider.notifier).connect(); - await _handleBetaTimelineResume(); - } + _ref.read(websocketProvider.notifier).connect(); + await _handleBetaTimelineResume(); await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission(); await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - - if (!Store.isBetaTimelineEnabled) { - await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - - _ref.invalidate(memoryFutureProvider); - } } Future _safeRun(Future action, String debugName) async { @@ -139,7 +100,6 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _handleBetaTimelineResume() async { - _ref.read(backupProvider.notifier).cancelBackup(); unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock()); // Give isolates time to complete any ongoing database transactions @@ -218,9 +178,7 @@ class AppLifeCycleNotifier extends StateNotifier { _pauseOperation = Completer(); try { - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); await _performPause(); } catch (e, stackTrace) { _log.severe("Error during app pause", e, stackTrace); @@ -234,14 +192,7 @@ class AppLifeCycleNotifier extends StateNotifier { Future _performPause() { if (_ref.read(authProvider).isAuthenticated) { - if (!Store.isBetaTimelineEnabled) { - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); - } - } else { - _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); - } + _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); _ref.read(websocketProvider.notifier).disconnect(); } @@ -252,31 +203,12 @@ class AppLifeCycleNotifier extends StateNotifier { Future handleAppDetached() async { state = AppLifeCycleEnum.detached; - if (Store.isBetaTimelineEnabled) { - unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); - } + unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock()); // Flush logs before closing database try { await LogService.I.flush(); } catch (_) {} - - // Close Isar database safely - try { - final isar = Isar.getInstance(); - if (isar != null && isar.isOpen) { - await isar.close(); - } - } catch (_) {} - - if (Store.isBetaTimelineEnabled) { - return; - } - - // no guarantee this is called at all - try { - _ref.read(manualUploadProvider.notifier).cancelBackup(); - } catch (_) {} } void handleAppHidden() { diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart deleted file mode 100644 index d5a4e42b74..0000000000 --- a/mobile/lib/providers/asset.provider.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/etag.service.dart'; -import 'package:immich_mobile/services/exif.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetProvider = StateNotifierProvider((ref) { - return AssetNotifier( - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(etagServiceProvider), - ref.watch(exifServiceProvider), - ref, - ); -}); - -class AssetNotifier extends StateNotifier { - final AssetService _assetService; - final AlbumService _albumService; - final UserService _userService; - final SyncService _syncService; - final ETagService _etagService; - final ExifService _exifService; - final Ref _ref; - final log = Logger('AssetNotifier'); - bool _getAllAssetInProgress = false; - bool _deleteInProgress = false; - - AssetNotifier( - this._assetService, - this._albumService, - this._userService, - this._syncService, - this._etagService, - this._exifService, - this._ref, - ) : super(false); - - Future getAllAsset({bool clear = false}) async { - if (_getAllAssetInProgress || _deleteInProgress) { - // guard against multiple calls to this method while it's still working - return; - } - final stopwatch = Stopwatch()..start(); - try { - _getAllAssetInProgress = true; - state = true; - if (clear) { - await clearAllAssets(); - log.info("Manual refresh requested, cleared assets and albums from db"); - } - final users = await _syncService.getUsersFromServer(); - bool changedUsers = false; - if (users != null) { - changedUsers = await _syncService.syncUsersFromServer(users); - } - final bool newRemote = await _assetService.refreshRemoteAssets(); - final bool newLocal = await _albumService.refreshDeviceAlbums(); - dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); - if (newRemote) { - _ref.invalidate(memoryFutureProvider); - } - - log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); - } catch (error) { - // If there is error in getting the remote assets, still showing the new local assets - await _albumService.refreshDeviceAlbums(); - } finally { - _getAllAssetInProgress = false; - if (mounted) { - state = false; - } - } - } - - Future clearAllAssets() async { - await Store.delete(StoreKey.assetETag); - await Future.wait([ - _assetService.clearTable(), - _exifService.clearTable(), - _albumService.clearTable(), - _userService.deleteAll(), - _etagService.clearTable(), - ]); - } - - Future onNewAssetUploaded(Asset newAsset) async { - // eTag on device is not valid after partially modifying the assets - await Store.delete(StoreKey.assetETag); - await _syncService.syncNewAssetToDb(newAsset); - } - - Future deleteLocalAssets(List assets) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteLocalAssets(assets); - return true; - } catch (error) { - log.severe("Failed to delete local assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - /// Delete remote asset only - /// - /// Default behavior is trashing the asset - Future deleteRemoteAssets(Iterable deleteAssets, {bool shouldDeletePermanently = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteRemoteAssets(deleteAssets, shouldDeletePermanently: shouldDeletePermanently); - return true; - } catch (error) { - log.severe("Failed to delete remote assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future deleteAssets(Iterable deleteAssets, {bool force = false}) async { - _deleteInProgress = true; - state = true; - try { - await _assetService.deleteAssets(deleteAssets, shouldDeletePermanently: force); - return true; - } catch (error) { - log.severe("Failed to delete assets", error); - return false; - } finally { - _deleteInProgress = false; - state = false; - } - } - - Future toggleFavorite(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isFavorite); - return _assetService.changeFavoriteStatus(assets, status); - } - - Future toggleArchive(List assets, [bool? status]) { - status ??= !assets.every((a) => a.isArchived); - return _assetService.changeArchiveStatus(assets, status); - } - - Future setLockedView(List selection, AssetVisibilityEnum visibility) { - return _assetService.setVisibility(selection, visibility); - } -} - -final assetDetailProvider = StreamProvider.autoDispose.family((ref, asset) async* { - final assetService = ref.watch(assetServiceProvider); - yield await assetService.loadExif(asset); - - await for (final asset in assetService.watchAsset(asset.id)) { - if (asset != null) { - yield await ref.watch(assetServiceProvider).loadExif(asset); - } - } -}); - -final assetWatcher = StreamProvider.autoDispose.family((ref, asset) { - final assetService = ref.watch(assetServiceProvider); - return assetService.watchAsset(asset.id, fireImmediately: true); -}); diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.dart deleted file mode 100644 index e2227920c7..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_people.provider.g.dart'; - -/// Maintains the list of people for an asset. -@riverpod -class AssetPeopleNotifier extends _$AssetPeopleNotifier { - final log = Logger('AssetPeopleNotifier'); - - @override - Future> build(Asset asset) async { - if (!asset.isRemote) { - return []; - } - - final list = await ref.watch(assetServiceProvider).getRemotePeopleOfAsset(asset.remoteId!); - if (list == null) { - return []; - } - - // explicitly a sorted slice to make it deterministic - // named people will be at the beginning, and names are sorted - // ascendingly - list.sort((a, b) { - final aNotEmpty = a.name.isNotEmpty; - final bNotEmpty = b.name.isNotEmpty; - if (aNotEmpty && !bNotEmpty) { - return -1; - } else if (!aNotEmpty && bNotEmpty) { - return 1; - } else if (!aNotEmpty && !bNotEmpty) { - return 0; - } - - return a.name.compareTo(b.name); - }); - return list; - } - - Future refresh() async { - // invalidate the state – this way we don't have to - // duplicate the code from build. - ref.invalidateSelf(); - } -} diff --git a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart deleted file mode 100644 index 031a70e0d9..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_people.provider.g.dart +++ /dev/null @@ -1,192 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_people.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetPeopleNotifierHash() => - r'9835b180984a750c91e923e7b64dbda94f6d7574'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$AssetPeopleNotifier - extends - BuildlessAutoDisposeAsyncNotifier> { - late final Asset asset; - - FutureOr> build(Asset asset); -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -@ProviderFor(AssetPeopleNotifier) -const assetPeopleNotifierProvider = AssetPeopleNotifierFamily(); - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierFamily - extends Family>> { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - const AssetPeopleNotifierFamily(); - - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider call(Asset asset) { - return AssetPeopleNotifierProvider(asset); - } - - @override - AssetPeopleNotifierProvider getProviderOverride( - covariant AssetPeopleNotifierProvider provider, - ) { - return call(provider.asset); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'assetPeopleNotifierProvider'; -} - -/// Maintains the list of people for an asset. -/// -/// Copied from [AssetPeopleNotifier]. -class AssetPeopleNotifierProvider - extends - AutoDisposeAsyncNotifierProviderImpl< - AssetPeopleNotifier, - List - > { - /// Maintains the list of people for an asset. - /// - /// Copied from [AssetPeopleNotifier]. - AssetPeopleNotifierProvider(Asset asset) - : this._internal( - () => AssetPeopleNotifier()..asset = asset, - from: assetPeopleNotifierProvider, - name: r'assetPeopleNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetPeopleNotifierHash, - dependencies: AssetPeopleNotifierFamily._dependencies, - allTransitiveDependencies: - AssetPeopleNotifierFamily._allTransitiveDependencies, - asset: asset, - ); - - AssetPeopleNotifierProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.asset, - }) : super.internal(); - - final Asset asset; - - @override - FutureOr> runNotifierBuild( - covariant AssetPeopleNotifier notifier, - ) { - return notifier.build(asset); - } - - @override - Override overrideWith(AssetPeopleNotifier Function() create) { - return ProviderOverride( - origin: this, - override: AssetPeopleNotifierProvider._internal( - () => create()..asset = asset, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - asset: asset, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - createElement() { - return _AssetPeopleNotifierProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is AssetPeopleNotifierProvider && other.asset == asset; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, asset.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin AssetPeopleNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { - /// The parameter `asset` of this provider. - Asset get asset; -} - -class _AssetPeopleNotifierProviderElement - extends - AutoDisposeAsyncNotifierProviderElement< - AssetPeopleNotifier, - List - > - with AssetPeopleNotifierRef { - _AssetPeopleNotifierProviderElement(super.provider); - - @override - Asset get asset => (origin as AssetPeopleNotifierProvider).asset; -} - -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart deleted file mode 100644 index 8772e3d0cb..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'asset_stack.provider.g.dart'; - -class AssetStackNotifier extends StateNotifier> { - final AssetService assetService; - final String _stackId; - - AssetStackNotifier(this.assetService, this._stackId) : super([]) { - _fetchStack(_stackId); - } - - void _fetchStack(String stackId) async { - if (!mounted) { - return; - } - - final stack = await assetService.getStackAssets(stackId); - if (stack.isNotEmpty) { - state = stack; - } - } - - void removeChild(int index) { - if (index < state.length) { - state.removeAt(index); - state = List.from(state); - } - } -} - -final assetStackStateProvider = StateNotifierProvider.autoDispose.family, String>( - (ref, stackId) => AssetStackNotifier(ref.watch(assetServiceProvider), stackId), -); - -@riverpod -int assetStackIndex(Ref _) { - return -1; -} diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart deleted file mode 100644 index dcf82cdebd..0000000000 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'asset_stack.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$assetStackIndexHash() => r'086ddb782e3eb38b80d755666fe35be8fe7322d7'; - -/// See also [assetStackIndex]. -@ProviderFor(assetStackIndex) -final assetStackIndexProvider = AutoDisposeProvider.internal( - assetStackIndex, - name: r'assetStackIndexProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$assetStackIndexHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef AssetStackIndexRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.dart deleted file mode 100644 index 0e25660ab0..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_asset.provider.g.dart'; - -@riverpod -class CurrentAsset extends _$CurrentAsset { - @override - Asset? build() => null; - - void set(Asset? a) => state = a; -} - -/// Mock class for testing -abstract class CurrentAssetInternal extends _$CurrentAsset {} diff --git a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart b/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart deleted file mode 100644 index e0d8d47d3a..0000000000 --- a/mobile/lib/providers/asset_viewer/current_asset.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_asset.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentAssetHash() => r'2def10ea594152c984ae2974d687ab6856d7bdd0'; - -/// See also [CurrentAsset]. -@ProviderFor(CurrentAsset) -final currentAssetProvider = - AutoDisposeNotifierProvider.internal( - CurrentAsset.new, - name: r'currentAssetProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentAssetHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$CurrentAsset = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index a461d5766a..25db76b077 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -1,26 +1,15 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/download/download_state.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; class DownloadStateNotifier extends StateNotifier { final DownloadService _downloadService; - final ShareService _shareService; - final AlbumService _albumService; - DownloadStateNotifier(this._downloadService, this._shareService, this._albumService) + DownloadStateNotifier(this._downloadService) : super( const DownloadState( downloadStatus: TaskStatus.complete, @@ -132,18 +121,9 @@ class DownloadStateNotifier extends StateNotifier { if (state.taskProgress.isEmpty) { state = state.copyWith(showProgress: false); } - _albumService.refreshDeviceAlbums(); }); } - Future> downloadAllAsset(List assets) async { - return await _downloadService.downloadAll(assets); - } - - void downloadAsset(Asset asset) async { - await _downloadService.download(asset); - } - void cancelDownload(String id) async { final isCanceled = await _downloadService.cancelDownload(id); @@ -159,36 +139,8 @@ class DownloadStateNotifier extends StateNotifier { state = state.copyWith(showProgress: false); } } - - void shareAsset(Asset asset, BuildContext context) async { - unawaited( - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService.shareAsset(asset, context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ), - ); - } } final downloadStateProvider = StateNotifierProvider( - ((ref) => DownloadStateNotifier( - ref.watch(downloadServiceProvider), - ref.watch(shareServiceProvider), - ref.watch(albumServiceProvider), - )), + ((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))), ); diff --git a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart b/mobile/lib/providers/asset_viewer/render_list_status_provider.dart deleted file mode 100644 index 189ac85452..0000000000 --- a/mobile/lib/providers/asset_viewer/render_list_status_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -enum RenderListStatusEnum { complete, empty, error, loading } - -final renderListStatusProvider = StateNotifierProvider((ref) { - return RenderListStatus(ref); -}); - -class RenderListStatus extends StateNotifier { - RenderListStatus(this.ref) : super(RenderListStatusEnum.complete); - - final Ref ref; - - RenderListStatusEnum get status => state; - - set status(RenderListStatusEnum value) { - state = value; - } -} diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 5f3ad3d058..a6dc272313 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,672 +1,23 @@ import 'dart:async'; -import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/auth/auth_state.model.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; -final backupProvider = StateNotifierProvider((ref) { - return BackupNotifier( - ref.watch(backupServiceProvider), - ref.watch(serverInfoServiceProvider), - ref.watch(authProvider), - ref.watch(backgroundServiceProvider), - ref.watch(galleryPermissionNotifier.notifier), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); +final backupProvider = StateNotifierProvider((ref) { + return BackupNotifier(ref.watch(serverInfoServiceProvider)); }); -class BackupNotifier extends StateNotifier { - BackupNotifier( - this._backupService, - this._serverInfoService, - this._authState, - this._backgroundService, - this._galleryPermissionNotifier, - this._albumMediaRepository, - this._fileMediaRepository, - this._backupAlbumService, - this.ref, - ) : super( - BackUpState( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: const [], - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - autoBackup: Store.get(StoreKey.autoBackup, false), - backgroundBackup: Store.get(StoreKey.backgroundBackup, false), - backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), - backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false), - backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000), - serverInfo: const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0), - availableAlbums: const [], - selectedBackupAlbums: const {}, - excludedBackupAlbums: const {}, - allUniqueAssets: const {}, - selectedAlbumsBackupAssetsIds: const {}, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - fileSize: 0, - iCloudAsset: false, - ), - iCloudDownloadProgress: 0.0, - ), - ); +class BackupNotifier extends StateNotifier { + BackupNotifier(this._serverInfoService) + : super(const ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0)); - final log = Logger('BackupNotifier'); - final BackupService _backupService; final ServerInfoService _serverInfoService; - final AuthState _authState; - final BackgroundService _backgroundService; - final GalleryPermissionNotifier _galleryPermissionNotifier; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - /// - /// UI INTERACTION - /// - /// Album selection - /// Due to the overlapping assets across multiple albums on the device - /// We have method to include and exclude albums - /// The total unique assets will be used for backing mechanism - /// - void addAlbumForBackup(AvailableAlbum album) { - if (state.excludedBackupAlbums.contains(album)) { - removeExcludedAlbumForBackup(album); - } - - state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album}); - } - - void addExcludedAlbumForBackup(AvailableAlbum album) { - if (state.selectedBackupAlbums.contains(album)) { - removeAlbumForBackup(album); - } - state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album}); - } - - void removeAlbumForBackup(AvailableAlbum album) { - Set currentSelectedAlbums = state.selectedBackupAlbums; - - currentSelectedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums); - } - - void removeExcludedAlbumForBackup(AvailableAlbum album) { - Set currentExcludedAlbums = state.excludedBackupAlbums; - - currentExcludedAlbums.removeWhere((a) => a == album); - - state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums); - } - - Future backupAlbumSelectionDone() { - if (state.selectedBackupAlbums.isEmpty) { - // disable any backup - cancelBackup(); - setAutoBackup(false); - configureBackgroundBackup(enabled: false, onError: (msg) {}, onBatteryInfo: () {}); - } - return _updateBackupAssetCount(); - } - - void setAutoBackup(bool enabled) { - Store.put(StoreKey.autoBackup, enabled); - state = state.copyWith(autoBackup: enabled); - } - - void configureBackgroundBackup({ - bool? enabled, - bool? requireWifi, - bool? requireCharging, - int? triggerDelay, - required void Function(String msg) onError, - required void Function() onBatteryInfo, - }) async { - assert(enabled != null || requireWifi != null || requireCharging != null || triggerDelay != null); - final bool wasEnabled = state.backgroundBackup; - final bool wasWifi = state.backupRequireWifi; - final bool wasCharging = state.backupRequireCharging; - final int oldTriggerDelay = state.backupTriggerDelay; - state = state.copyWith( - backgroundBackup: enabled, - backupRequireWifi: requireWifi, - backupRequireCharging: requireCharging, - backupTriggerDelay: triggerDelay, - ); - - if (state.backgroundBackup) { - bool success = true; - if (!wasEnabled) { - if (!await _backgroundService.isIgnoringBatteryOptimizations()) { - onBatteryInfo(); - } - success &= await _backgroundService.enableService(immediate: true); - } - success &= - success && - await _backgroundService.configureService( - requireUnmetered: state.backupRequireWifi, - requireCharging: state.backupRequireCharging, - triggerUpdateDelay: state.backupTriggerDelay, - triggerMaxDelay: state.backupTriggerDelay * 10, - ); - if (success) { - await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi); - await Store.put(StoreKey.backupRequireCharging, state.backupRequireCharging); - await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay); - await Store.put(StoreKey.backgroundBackup, state.backgroundBackup); - } else { - state = state.copyWith( - backgroundBackup: wasEnabled, - backupRequireWifi: wasWifi, - backupRequireCharging: wasCharging, - backupTriggerDelay: oldTriggerDelay, - ); - onError("backup_controller_page_background_configure_error"); - } - } else { - final bool success = await _backgroundService.disableService(); - if (!success) { - state = state.copyWith(backgroundBackup: wasEnabled); - onError("backup_controller_page_background_configure_error"); - } - } - } - - /// - /// Get all album on the device - /// Get all selected and excluded album from the user's persistent storage - /// If this is the first time performing backup - set the default selected album to be - /// the one that has all assets (`Recent` on Android, `Recents` on iOS) - /// - Future _getBackupAlbumsInfo() async { - Stopwatch stopwatch = Stopwatch()..start(); - // Get all albums on the device - List availableAlbums = []; - List albums = await _albumMediaRepository.getAll(); - - // Map of id -> album for quick album lookup later on. - Map albumMap = {}; - - log.info('Found ${albums.length} local albums'); - - for (Album album in albums) { - AvailableAlbum availableAlbum = AvailableAlbum( - album: album, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(album.localId!), - ); - - availableAlbums.add(availableAlbum); - - albumMap[album.localId!] = album; - } - state = state.copyWith(availableAlbums: availableAlbums); - - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - - final Set selectedAlbums = {}; - for (final BackupAlbum ba in selectedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - selectedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await _albumMediaRepository.getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Selected album not found'); - } - } - - final Set excludedAlbums = {}; - for (final BackupAlbum ba in excludedBackupAlbums) { - final albumAsset = albumMap[ba.id]; - - if (albumAsset != null) { - excludedAlbums.add( - AvailableAlbum( - album: albumAsset, - assetCount: await ref.read(albumMediaRepositoryProvider).getAssetCount(albumAsset.localId!), - lastBackup: ba.lastBackup, - ), - ); - } else { - log.severe('Excluded album not found'); - } - } - - state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); - - log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); - dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); - } - - /// - /// From all the selected and albums assets - /// Find the assets that are not overlapping between the two sets - /// Those assets are unique and are used as the total assets - /// - Future _updateBackupAssetCount() async { - // Save to persistent storage - await _updatePersistentAlbumsSelection(); - - final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; - - for (final album in state.selectedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [album.name]; - - final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull((a) => a.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - assetsFromSelectedAlbums.remove(existingAsset); - } - - assetsFromSelectedAlbums.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - } - - for (final album in state.excludedBackupAlbums) { - final assetCount = await ref.read(albumMediaRepositoryProvider).getAssetCount(album.album.localId!); - - if (assetCount == 0) { - continue; - } - - final assets = await ref.read(albumMediaRepositoryProvider).getAssets(album.album.localId!); - - for (final asset in assets) { - assetsFromExcludedAlbums.add(BackupCandidate(asset: asset, albumNames: [album.name])); - } - } - - final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); - - final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); - - if (allAssetsInDatabase == null) { - return; - } - - // Find asset that were backup from selected albums - final Set selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.asset.localId)); - - selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); - - // Remove duplicated asset from all unique assets - allUniqueAssets.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (allUniqueAssets.isEmpty) { - log.info("No assets are selected for back up"); - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: {}, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } else { - state = state.copyWith( - allAssetsInDatabase: allAssetsInDatabase, - allUniqueAssets: allUniqueAssets, - selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, - ); - } - } - - /// Get all necessary information for calculating the available albums, - /// which albums are selected or excluded - /// and then update the UI according to those information - Future getBackupInfo() async { - final isEnabled = await _backgroundService.isBackgroundBackupEnabled(); - - state = state.copyWith(backgroundBackup: isEnabled); - if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) { - await Store.put(StoreKey.backgroundBackup, isEnabled); - } - - if (state.backupProgress != BackUpProgressEnum.inBackground) { - await _getBackupAlbumsInfo(); - await updateDiskInfo(); - await _updateBackupAssetCount(); - } else { - log.warning("cannot get backup info - background backup is in progress!"); - } - } - - /// Save user selection of selected albums and excluded albums to database - Future _updatePersistentAlbumsSelection() async { - final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); - final selected = state.selectedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select), - ); - final excluded = state.excludedBackupAlbums.map( - (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude), - ); - final candidates = selected.followedBy(excluded).toList(); - candidates.sortBy((e) => e.id); - - final savedBackupAlbums = await _backupAlbumService.getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - - diffSortedListsSync( - savedBackupAlbums, - candidates, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - b.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(b); - return true; - }, - onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), - onlySecond: (BackupAlbum b) => toUpsert.add(b), - ); - - await _backupAlbumService.deleteAll(toDelete); - await _backupAlbumService.updateAll(toUpsert); - } - - /// Invoke backup process - Future startBackupProcess() async { - dPrint(() => "Start backup process"); - assert(state.backupProgress == BackUpProgressEnum.idle); - state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); - - await getBackupInfo(); - - final hasPermission = _galleryPermissionNotifier.hasPermission; - if (hasPermission) { - await _fileMediaRepository.clearFileCache(); - - if (state.allUniqueAssets.isEmpty) { - log.info("No Asset On Device - Abort Backup Process"); - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - return; - } - - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); - // Remove item that has already been backed up - for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId); - } - - if (assetsWillBeBackup.isEmpty) { - state = state.copyWith(backupProgress: BackUpProgressEnum.idle); - } - - // Perform Backup - _cancelToken?.complete(); - _cancelToken = Completer(); - - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - pmProgressHandler?.stream.listen((event) { - final double progress = event.progress; - state = state.copyWith(iCloudDownloadProgress: progress); - }); - - await _backupService.backupAsset( - assetsWillBeBackup, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onUploadProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onBackupError, - ); - await notifyBackgroundServiceCanRun(); - } else { - await openAppSettings(); - } - } - - void setAvailableAlbums(availableAlbums) { - state = state.copyWith(availableAlbums: availableAlbums); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset); - } - - void cancelBackup() { - if (state.backupProgress != BackUpProgressEnum.inProgress) { - notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - state = state.copyWith( - backupProgress: BackUpProgressEnum.idle, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - void _onAssetUploaded(SuccessUploadAsset result) async { - if (result.isDuplicate) { - state = state.copyWith( - allUniqueAssets: state.allUniqueAssets - .where((candidate) => candidate.asset.localId != result.candidate.asset.localId) - .toSet(), - ); - } else { - state = state.copyWith( - selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, result.candidate.asset.localId!}, - allAssetsInDatabase: [...state.allAssetsInDatabase, result.candidate.asset.localId!], - ); - } - - if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = state.allUniqueAssets - .map((candidate) => candidate.asset.fileModifiedAt) - .reduce((v, e) => e.isAfter(v) ? e : v); - state = state.copyWith( - selectedBackupAlbums: state.selectedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - excludedBackupAlbums: state.excludedBackupAlbums.map((e) => e.copyWith(lastBackup: latestAssetBackup)).toSet(), - backupProgress: BackUpProgressEnum.done, - progressInPercentage: 0.0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - await _updatePersistentAlbumsSelection(); - } - - await updateDiskInfo(); - } - - void _onUploadProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - } Future updateDiskInfo() async { final diskInfo = await _serverInfoService.getDiskInfo(); - - // Update server info if (diskInfo != null) { - state = state.copyWith(serverInfo: diskInfo); + state = diskInfo; } } - - Future _resumeBackup() async { - // Check if user is login - final accessKey = Store.tryGet(StoreKey.accessToken); - - // User has been logged out return - if (accessKey == null || !_authState.isAuthenticated) { - log.info("[_resumeBackup] not authenticated - abort"); - return; - } - - // Check if this device is enable backup by the user - if (state.autoBackup) { - // check if backup is already in process - then return - if (state.backupProgress == BackUpProgressEnum.inProgress) { - log.info("[_resumeBackup] Auto Backup is already in progress - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.inBackground) { - log.info("[_resumeBackup] Background backup is running - abort"); - return; - } - - if (state.backupProgress == BackUpProgressEnum.manualInProgress) { - log.info("[_resumeBackup] Manual upload is running - abort"); - return; - } - - // Run backup - log.info("[_resumeBackup] Start back up"); - await startBackupProcess(); - } - return; - } - - Future resumeBackup() async { - final List selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final List excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - Set selectedAlbums = state.selectedBackupAlbums; - Set excludedAlbums = state.excludedBackupAlbums; - if (selectedAlbums.isNotEmpty) { - selectedAlbums = _updateAlbumsBackupTime(selectedAlbums, selectedBackupAlbums); - } - - if (excludedAlbums.isNotEmpty) { - excludedAlbums = _updateAlbumsBackupTime(excludedAlbums, excludedBackupAlbums); - } - final BackUpProgressEnum previous = state.backupProgress; - state = state.copyWith( - backupProgress: BackUpProgressEnum.inBackground, - selectedBackupAlbums: selectedAlbums, - excludedBackupAlbums: excludedAlbums, - ); - // assumes the background service is currently running - // if true, waits until it has stopped to start the backup - final bool hasLock = await _backgroundService.acquireLock(); - if (hasLock) { - state = state.copyWith(backupProgress: previous); - } - return _resumeBackup(); - } - - Set _updateAlbumsBackupTime(Set albums, List backupAlbums) { - Set result = {}; - for (BackupAlbum ba in backupAlbums) { - try { - AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id); - result.add(a.copyWith(lastBackup: ba.lastBackup)); - } on StateError { - log.severe("[_updateAlbumBackupTime] failed to find album in state", "State Error", StackTrace.current); - } - } - return result; - } - - Future notifyBackgroundServiceCanRun() async { - const allowedStates = [AppLifeCycleEnum.inactive, AppLifeCycleEnum.paused, AppLifeCycleEnum.detached]; - if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) { - _backgroundService.releaseLock(); - } - } - - BackUpProgressEnum get backupProgress => state.backupProgress; - - void updateBackupProgress(BackUpProgressEnum backupProgress) { - state = state.copyWith(backupProgress: backupProgress); - } } diff --git a/mobile/lib/providers/backup/backup_verification.provider.dart b/mobile/lib/providers/backup/backup_verification.provider.dart deleted file mode 100644 index 50270e87ca..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/services/backup_verification.service.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -part 'backup_verification.provider.g.dart'; - -@riverpod -class BackupVerification extends _$BackupVerification { - @override - bool build() => false; - - void performBackupCheck(BuildContext context) async { - try { - state = true; - final backupState = ref.read(backupProvider); - - if (backupState.allUniqueAssets.length > backupState.selectedAlbumsBackupAssetsIds.length) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Backup all assets before starting this check!", - toastType: ToastType.error, - ); - } - return; - } - final connection = await Connectivity().checkConnectivity(); - if (!connection.contains(ConnectivityResult.wifi)) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Make sure to be connected to unmetered Wi-Fi", - toastType: ToastType.error, - ); - } - return; - } - unawaited(WakelockPlus.enable()); - - const limit = 100; - final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit); - if (toDelete.isEmpty) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "Did not find any corrupt asset backups!", - toastType: ToastType.success, - ); - } - } else { - if (context.mounted) { - await showDialog( - context: context, - builder: (ctx) => ConfirmDialog( - onOk: () => _performDeletion(context, toDelete), - title: "Corrupt backups!", - ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " - "Run the check again to find more.\n" - "Do you want to delete the corrupt asset backups now?", - ), - ); - } - } - } finally { - unawaited(WakelockPlus.disable()); - state = false; - } - } - - Future _performDeletion(BuildContext context, List assets) async { - try { - state = true; - if (context.mounted) { - ImmichToast.show(context: context, msg: "Deleting ${assets.length} assets on the server..."); - } - await ref.read(assetProvider.notifier).deleteAssets(assets, force: true); - if (context.mounted) { - ImmichToast.show( - context: context, - msg: - "Deleted ${assets.length} assets on the server. " - "You can now start a manual backup", - toastType: ToastType.success, - ); - } - } finally { - state = false; - } - } -} diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart deleted file mode 100644 index 13f6819fa7..0000000000 --- a/mobile/lib/providers/backup/backup_verification.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'backup_verification.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$backupVerificationHash() => - r'b4b34909ed1af3f28877ea457d53a4a18b6417f8'; - -/// See also [BackupVerification]. -@ProviderFor(BackupVerification) -final backupVerificationProvider = - AutoDisposeNotifierProvider.internal( - BackupVerification.new, - name: r'backupVerificationProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$backupVerificationHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$BackupVerification = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/backup/error_backup_list.provider.dart b/mobile/lib/providers/backup/error_backup_list.provider.dart deleted file mode 100644 index db116e4bb9..0000000000 --- a/mobile/lib/providers/backup/error_backup_list.provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; - -class ErrorBackupListNotifier extends StateNotifier> { - ErrorBackupListNotifier() : super({}); - - add(ErrorUploadAsset errorAsset) { - state = state.union({errorAsset}); - } - - remove(ErrorUploadAsset errorAsset) { - state = state.difference({errorAsset}); - } - - empty() { - state = {}; - } -} - -final errorBackupListProvider = StateNotifierProvider>( - (ref) => ErrorBackupListNotifier(), -); diff --git a/mobile/lib/providers/backup/ios_background_settings.provider.dart b/mobile/lib/providers/backup/ios_background_settings.provider.dart deleted file mode 100644 index 98d55882cc..0000000000 --- a/mobile/lib/providers/backup/ios_background_settings.provider.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/background.service.dart'; - -class IOSBackgroundSettings { - final bool appRefreshEnabled; - final int numberOfBackgroundTasksQueued; - final DateTime? timeOfLastFetch; - final DateTime? timeOfLastProcessing; - - const IOSBackgroundSettings({ - required this.appRefreshEnabled, - required this.numberOfBackgroundTasksQueued, - this.timeOfLastFetch, - this.timeOfLastProcessing, - }); -} - -class IOSBackgroundSettingsNotifier extends StateNotifier { - final BackgroundService _service; - IOSBackgroundSettingsNotifier(this._service) : super(null); - - IOSBackgroundSettings? get settings => state; - - Future refresh() async { - final lastFetchTime = await _service.getIOSBackupLastRun(IosBackgroundTask.fetch); - final lastProcessingTime = await _service.getIOSBackupLastRun(IosBackgroundTask.processing); - int numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - final appRefreshEnabled = await _service.getIOSBackgroundAppRefreshEnabled(); - - // If this is enabled and there are no background processes, - // the user just enabled app refresh in Settings. - // But we don't have any background services running, since it was disabled - // before. - if (await _service.isBackgroundBackupEnabled() && numberOfProcesses == 0) { - // We need to restart the background service - await _service.enableService(); - numberOfProcesses = await _service.getIOSBackupNumberOfProcesses(); - } - - final settings = IOSBackgroundSettings( - appRefreshEnabled: appRefreshEnabled, - numberOfBackgroundTasksQueued: numberOfProcesses, - timeOfLastFetch: lastFetchTime, - timeOfLastProcessing: lastProcessingTime, - ); - - state = settings; - return settings; - } -} - -final iOSBackgroundSettingsProvider = StateNotifierProvider( - (ref) => IOSBackgroundSettingsNotifier(ref.watch(backgroundServiceProvider)), -); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart deleted file mode 100644 index 40efcd7422..0000000000 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ /dev/null @@ -1,391 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/manual_upload_state.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/backup_album.service.dart'; -import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final manualUploadProvider = StateNotifierProvider((ref) { - return ManualUploadNotifier( - ref.watch(localNotificationService), - ref.watch(backupProvider.notifier), - ref.watch(backupServiceProvider), - ref.watch(backupAlbumServiceProvider), - ref, - ); -}); - -class ManualUploadNotifier extends StateNotifier { - final Logger _log = Logger("ManualUploadNotifier"); - final LocalNotificationService _localNotificationService; - final BackupNotifier _backupProvider; - final BackupService _backupService; - final BackupAlbumService _backupAlbumService; - final Ref ref; - Completer? _cancelToken; - - ManualUploadNotifier( - this._localNotificationService, - this._backupProvider, - this._backupService, - this._backupAlbumService, - this.ref, - ) : super( - ManualUploadState( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeeds: const [], - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - totalAssetsToUpload: 0, - successfulUploads: 0, - currentAssetIndex: 0, - showDetailedNotification: false, - ), - ); - - String _lastPrintedDetailContent = ''; - String? _lastPrintedDetailTitle; - - static const notifyInterval = Duration(milliseconds: 500); - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - void _updateProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - _localNotificationService.showOrUpdateManualUploadStatus( - "backup_background_service_in_progress_notification".tr(), - formatAssetBackupProgress(state.currentAssetIndex, state.totalAssetsToUpload), - maxProgress: state.totalAssetsToUpload, - progress: state.currentAssetIndex, - showActions: true, - ); - } - } - - void _updateDetailProgress(String? title, int progress, int total) { - // Guard against throttling calling this method after the upload is done - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || title != _lastPrintedDetailTitle) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _localNotificationService.showOrUpdateManualUploadStatus( - title ?? 'Uploading', - msg, - progress: total > 0 ? (progress * 1000) ~/ total : 0, - maxProgress: 1000, - isDetailed: true, - // Detailed noitifcation is displayed for Single asset uploads. Show actions for such case - showActions: state.totalAssetsToUpload == 1, - ); - } - } - } - - void _onAssetUploaded(SuccessUploadAsset result) { - state = state.copyWith(successfulUploads: state.successfulUploads + 1); - _backupProvider.updateDiskInfo(); - } - - void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) { - ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); - } - - void _onProgress(int sent, int total) { - double lastUploadSpeed = state.progressInFileSpeed; - List lastUploadSpeeds = state.progressInFileSpeeds.toList(); - DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; - int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; - - final now = DateTime.now(); - final duration = now.difference(lastUpdateTime); - - // Keep the upload speed average span limited, to keep it somewhat relevant - if (lastUploadSpeeds.length > 10) { - lastUploadSpeeds.removeAt(0); - } - - if (duration.inSeconds > 0) { - lastUploadSpeeds.add(((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble()); - - lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); - lastUpdateTime = now; - lastSentBytes = sent; - } - - state = state.copyWith( - progressInPercentage: (sent.toDouble() / total.toDouble() * 100), - progressInFileSize: humanReadableFileBytesProgress(sent, total), - progressInFileSpeed: lastUploadSpeed, - progressInFileSpeeds: lastUploadSpeeds, - progressInFileSpeedUpdateTime: lastUpdateTime, - progressInFileSpeedUpdateSentBytes: lastSentBytes, - ); - - if (state.showDetailedNotification) { - final title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': state.currentUploadAsset.fileName}, - ); - _throttledDetailNotify(title: title, progress: sent, total: total); - } - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { - state = state.copyWith(currentUploadAsset: currentUploadAsset, currentAssetIndex: state.currentAssetIndex + 1); - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - if (state.showDetailedNotification) { - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - } - - Future _startUpload(Iterable allManualUploads) async { - bool hasErrors = false; - try { - _backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress); - - if (ref.read(galleryPermissionNotifier.notifier).hasPermission) { - await ref.read(fileMediaRepositoryProvider).clearFileCache(); - - final allAssetsFromDevice = allManualUploads.where((e) => e.isLocal && !e.isRemote).toList(); - - if (allAssetsFromDevice.length != allManualUploads.length) { - _log.warning( - '[_startUpload] Refreshed upload list -> ${allManualUploads.length - allAssetsFromDevice.length} asset will not be uploaded', - ); - } - - final selectedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.select); - final excludedBackupAlbums = await _backupAlbumService.getAllBySelection(BackupSelection.exclude); - - // Get candidates from selected albums and excluded albums - Set candidates = await _backupService.buildUploadCandidates( - selectedBackupAlbums, - excludedBackupAlbums, - useTimeFilter: false, - ); - - // Extrack candidate from allAssetsFromDevice - final uploadAssets = candidates.where( - (candidate) => - allAssetsFromDevice.firstWhereOrNull((asset) => asset.localId == candidate.asset.localId) != null, - ); - - if (uploadAssets.isEmpty) { - dPrint(() => "[_startUpload] No Assets to upload - Abort Process"); - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - return false; - } - - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - totalAssetsToUpload: uploadAssets.length, - successfulUploads: 0, - currentAssetIndex: 0, - currentUploadAsset: CurrentUploadAsset( - id: '...', - fileCreatedAt: DateTime.parse('2020-10-04'), - fileName: '...', - fileType: '...', - ), - ); - // Reset Error List - ref.watch(errorBackupListProvider.notifier).empty(); - - if (state.totalAssetsToUpload > 1) { - _throttledNotifiy(); - } - - // Show detailed asset if enabled in settings or if a single asset is uploaded - bool showDetailedNotification = - ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.backgroundBackupSingleProgress) || - state.totalAssetsToUpload == 1; - state = state.copyWith(showDetailedNotification: showDetailedNotification); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - _cancelToken?.complete(); - _cancelToken = Completer(); - final bool ok = await ref - .read(backupServiceProvider) - .backupAsset( - uploadAssets, - _cancelToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onAssetUploadError, - ); - - // Close detailed notification - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - - _log.info( - '[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},' - ' failed: ${state.totalAssetsToUpload - state.successfulUploads}', - ); - - // User cancelled upload - if (!ok && _cancelToken == null) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_cancelled".tr(), - presentBanner: true, - ); - hasErrors = true; - } else if (state.successfulUploads == 0 || (!ok && _cancelToken != null)) { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "failed".tr(), - presentBanner: true, - ); - hasErrors = true; - } else { - await _localNotificationService.showOrUpdateManualUploadStatus( - "backup_manual_title".tr(), - "backup_manual_success".tr(), - presentBanner: true, - ); - } - } else { - unawaited(openAppSettings()); - dPrint(() => "[_startUpload] Do not have permission to the gallery"); - } - } catch (e) { - dPrint(() => "ERROR _startUpload: ${e.toString()}"); - hasErrors = true; - } finally { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - _handleAppInActivity(); - await _localNotificationService.closeNotification(LocalNotificationService.manualUploadDetailedNotificationID); - await _backupProvider.notifyBackgroundServiceCanRun(); - } - return !hasErrors; - } - - void _handleAppInActivity() { - final appState = ref.read(appStateProvider.notifier).getAppState(); - // The app is currently in background. Perform the necessary cleanups which - // are on-hold for upload completion - if (appState != AppLifeCycleEnum.active && appState != AppLifeCycleEnum.resumed) { - ref.read(backupProvider.notifier).cancelBackup(); - } - } - - void cancelBackup() { - if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress && - _backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.notifyBackgroundServiceCanRun(); - } - _cancelToken?.complete(); - _cancelToken = null; - if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { - _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); - } - state = state.copyWith( - progressInPercentage: 0, - progressInFileSize: "0 B / 0 B", - progressInFileSpeed: 0, - progressInFileSpeedUpdateTime: DateTime.now(), - progressInFileSpeedUpdateSentBytes: 0, - ); - } - - Future uploadAssets(BuildContext context, Iterable allManualUploads) async { - // assumes the background service is currently running and - // waits until it has stopped to start the backup. - final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); - if (!hasLock) { - dPrint(() => "[uploadAssets] could not acquire lock, exiting"); - ImmichToast.show( - context: context, - msg: "failed".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - return false; - } - - bool showInProgress = false; - - // check if backup is already in process - then return - if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - dPrint(() => "[uploadAssets] Manual upload is already running - abort"); - showInProgress = true; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { - dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort"); - showInProgress = true; - return false; - } - - if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { - dPrint(() => "[uploadAssets] Background backup is running - abort"); - showInProgress = true; - } - - if (showInProgress) { - if (context.mounted) { - ImmichToast.show( - context: context, - msg: "backup_manual_in_progress".tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - durationInSecond: 3, - ); - } - return false; - } - - return _startUpload(allManualUploads); - } -} diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index fea95f42aa..b298514d67 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/services/gcast.service.dart'; @@ -55,26 +54,6 @@ class CastNotifier extends StateNotifier { _gCastService.loadMedia(asset, reload); } - // TODO: remove this when we migrate to new timeline - void loadMediaOld(old_asset_entity.Asset asset, bool reload) { - final remoteAsset = RemoteAsset( - id: asset.remoteId.toString(), - name: asset.name, - ownerId: asset.ownerId.toString(), - checksum: asset.checksum, - type: asset.type == old_asset_entity.AssetType.image - ? AssetType.image - : asset.type == old_asset_entity.AssetType.video - ? AssetType.video - : AssetType.other, - createdAt: asset.fileCreatedAt, - updatedAt: asset.updatedAt, - isEdited: false, - ); - - _gCastService.loadMedia(remoteAsset, reload); - } - Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: diff --git a/mobile/lib/providers/db.provider.dart b/mobile/lib/providers/db.provider.dart deleted file mode 100644 index e03e037f36..0000000000 --- a/mobile/lib/providers/db.provider.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:isar/isar.dart'; - -// overwritten in main.dart due to async loading -final dbProvider = Provider((_) => throw UnimplementedError()); diff --git a/mobile/lib/providers/folder.provider.dart b/mobile/lib/providers/folder.provider.dart index 696d7e19fd..816a88996e 100644 --- a/mobile/lib/providers/folder.provider.dart +++ b/mobile/lib/providers/folder.provider.dart @@ -1,8 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/services/folder.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:logging/logging.dart'; class FolderStructureNotifier extends StateNotifier> { @@ -26,7 +26,7 @@ final folderStructureProvider = StateNotifierProvider> { +class FolderRenderListNotifier extends StateNotifier>> { final FolderService _folderService; final RootFolder _folder; final Logger _log = Logger("FolderAssetsNotifier"); @@ -36,8 +36,7 @@ class FolderRenderListNotifier extends StateNotifier> { Future fetchAssets(SortOrder order) async { try { final assets = await _folderService.getFolderAssets(_folder, order); - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.none); - state = AsyncData(renderList); + state = AsyncData(assets); } catch (e, stack) { _log.severe("Failed to fetch folder assets", e, stack); state = AsyncError(e, stack); @@ -46,6 +45,9 @@ class FolderRenderListNotifier extends StateNotifier> { } final folderRenderListProvider = - StateNotifierProvider.family, RootFolder>((ref, folder) { + StateNotifierProvider.family>, RootFolder>(( + ref, + folder, + ) { return FolderRenderListNotifier(ref.watch(folderServiceProvider), folder); }); diff --git a/mobile/lib/providers/image/exceptions/image_loading_exception.dart b/mobile/lib/providers/image/exceptions/image_loading_exception.dart deleted file mode 100644 index 98f633a88f..0000000000 --- a/mobile/lib/providers/image/exceptions/image_loading_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// An exception for the [ImageLoader] and the Immich image providers -class ImageLoadingException implements Exception { - final String message; - const ImageLoadingException(this.message); -} diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index bad0d986d0..434e930dcf 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -5,27 +5,26 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/download.service.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -final actionProvider = NotifierProvider( - ActionNotifier.new, - dependencies: [multiSelectProvider, timelineServiceProvider], -); +final actionProvider = NotifierProvider(ActionNotifier.new, dependencies: [multiSelectProvider]); class ActionResult { final int count; @@ -490,6 +489,29 @@ class ActionNotifier extends Notifier { }); } } + + Future applyEdits(ActionSource source, List edits) async { + final ids = _getOwnedRemoteIdsForSource(source); + + if (ids.length != 1) { + _logger.warning('applyEdits called with multiple assets, expected single asset'); + return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits'); + } + + final completer = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) { + final eventAsset = SyncAssetV1.fromJson(data["asset"]); + return eventAsset?.id == ids.first; + }, const Duration(seconds: 10)); + + try { + await _service.applyEdits(ids.first, edits); + await completer; + return const ActionResult(count: 1, success: true); + } catch (error, stack) { + _logger.severe('Failed to apply edits to assets', error, stack); + return ActionResult(count: ids.length, success: false, error: error.toString()); + } + } } extension on Iterable { diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index d38bcbfb55..2b4ba0129f 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -2,13 +2,6 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'db.provider.g.dart'; - -@Riverpod(keepAlive: true) -Isar isar(Ref ref) => throw UnimplementedError('isar'); Drift Function(Ref ref) driftOverride(Drift drift) => (ref) { ref.onDispose(() => unawaited(drift.close())); diff --git a/mobile/lib/providers/infrastructure/db.provider.g.dart b/mobile/lib/providers/infrastructure/db.provider.g.dart deleted file mode 100644 index 46abfb66a9..0000000000 --- a/mobile/lib/providers/infrastructure/db.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'db.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$isarHash() => r'69d3a06aa7e69a4381478e03f7956eb07d7f7feb'; - -/// See also [isar]. -@ProviderFor(isar) -final isarProvider = Provider.internal( - isar, - name: r'isarProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$isarHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef IsarRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/device_asset.provider.dart b/mobile/lib/providers/infrastructure/device_asset.provider.dart deleted file mode 100644 index 7854af016a..0000000000 --- a/mobile/lib/providers/infrastructure/device_asset.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; - -final deviceAssetRepositoryProvider = Provider( - (ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)), -); diff --git a/mobile/lib/providers/infrastructure/exif.provider.dart b/mobile/lib/providers/infrastructure/exif.provider.dart deleted file mode 100644 index c126f6cac0..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'exif.provider.g.dart'; - -@Riverpod(keepAlive: true) -IsarExifRepository exifRepository(Ref ref) => IsarExifRepository(ref.watch(isarProvider)); diff --git a/mobile/lib/providers/infrastructure/exif.provider.g.dart b/mobile/lib/providers/infrastructure/exif.provider.g.dart deleted file mode 100644 index 0261558707..0000000000 --- a/mobile/lib/providers/infrastructure/exif.provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exif.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463'; - -/// See also [exifRepository]. -@ProviderFor(exifRepository) -final exifRepositoryProvider = Provider.internal( - exifRepository, - name: r'exifRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$exifRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef ExifRepositoryRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart index 9e96c3cfc4..d503919c90 100644 --- a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart +++ b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -14,10 +15,11 @@ class ReadOnlyModeNotifier extends Notifier { } void setMode(bool value) { + final isLoggedIn = ref.read(authProvider).isAuthenticated; _appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); state = value; - if (value) { + if (value && isLoggedIn) { ref.read(appRouterProvider).navigate(const MainTimelineRoute()); } } diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart index 0bf42f3e8b..f867d30fdc 100644 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -1,13 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'store.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarStoreRepository storeRepository(Ref ref) => IsarStoreRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) StoreService storeService(Ref _) => StoreService.I; diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart index 98c978cb60..b5af7de3e0 100644 --- a/mobile/lib/providers/infrastructure/store.provider.g.dart +++ b/mobile/lib/providers/infrastructure/store.provider.g.dart @@ -6,23 +6,6 @@ part of 'store.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f'; - -/// See also [storeRepository]. -@ProviderFor(storeRepository) -final storeRepositoryProvider = Provider.internal( - storeRepository, - name: r'storeRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$storeRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef StoreRepositoryRef = ProviderRef; String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0'; /// See also [storeService]. diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index 922b9866bb..6c3263229e 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -3,7 +3,6 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -14,18 +13,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.provider.g.dart'; -@Riverpod(keepAlive: true) -IsarUserRepository userRepository(Ref ref) => IsarUserRepository(ref.watch(isarProvider)); - @Riverpod(keepAlive: true) UserApiRepository userApiRepository(Ref ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi); @Riverpod(keepAlive: true) -UserService userService(Ref ref) => UserService( - isarUserRepository: ref.watch(userRepositoryProvider), - userApiRepository: ref.watch(userApiRepositoryProvider), - storeService: ref.watch(storeServiceProvider), -); +UserService userService(Ref ref) => + UserService(userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider)); /// Drifts final driftPartnerRepositoryProvider = Provider( diff --git a/mobile/lib/providers/infrastructure/user.provider.g.dart b/mobile/lib/providers/infrastructure/user.provider.g.dart index f9148bf3a7..2e9115dad9 100644 --- a/mobile/lib/providers/infrastructure/user.provider.g.dart +++ b/mobile/lib/providers/infrastructure/user.provider.g.dart @@ -6,23 +6,6 @@ part of 'user.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3'; - -/// See also [userRepository]. -@ProviderFor(userRepository) -final userRepositoryProvider = Provider.internal( - userRepository, - name: r'userRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$userRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef UserRepositoryRef = ProviderRef; String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2'; /// See also [userApiRepository]. @@ -40,7 +23,7 @@ final userApiRepositoryProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef UserApiRepositoryRef = ProviderRef; -String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1'; +String _$userServiceHash() => r'47e607f3b484b51bcb634d47e3cbf1f6ef25da97'; /// See also [userService]. @ProviderFor(userService) diff --git a/mobile/lib/providers/memory.provider.dart b/mobile/lib/providers/memory.provider.dart deleted file mode 100644 index 7fef3060cc..0000000000 --- a/mobile/lib/providers/memory.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/services/memory.service.dart'; - -final memoryFutureProvider = FutureProvider.autoDispose?>((ref) async { - final service = ref.watch(memoryServiceProvider); - - return await service.getMemoryLane(); -}); diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart deleted file mode 100644 index 5a85cea1d4..0000000000 --- a/mobile/lib/providers/partner.provider.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; -import 'package:immich_mobile/services/partner.service.dart'; - -class PartnerSharedWithNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedWithNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedWith() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedWith().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) { - return _partnerService.updatePartner(partner, inTimeline: inTimeline); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedWithProvider = StateNotifierProvider>((ref) { - return PartnerSharedWithNotifier(ref.watch(partnerServiceProvider)); -}); - -class PartnerSharedByNotifier extends StateNotifier> { - final PartnerService _partnerService; - late final StreamSubscription> streamSub; - - PartnerSharedByNotifier(this._partnerService) : super([]) { - Function eq = const ListEquality().equals; - _partnerService - .getSharedBy() - .then((partners) { - if (!eq(state, partners)) { - state = partners; - } - }) - .then((_) { - streamSub = _partnerService.watchSharedBy().listen((partners) { - if (!eq(state, partners)) { - state = partners; - } - }); - }); - } - - @override - void dispose() { - if (mounted) { - streamSub.cancel(); - } - super.dispose(); - } -} - -final partnerSharedByProvider = StateNotifierProvider>((ref) { - return PartnerSharedByNotifier(ref.watch(partnerServiceProvider)); -}); - -final partnerAvailableProvider = FutureProvider.autoDispose>((ref) async { - final otherUsers = await ref.watch(otherUsersProvider.future); - final currentPartners = ref.watch(partnerSharedByProvider); - final available = Set.of(otherUsers); - available.removeAll(currentPartners); - return available.toList(); -}); diff --git a/mobile/lib/providers/search/all_motion_photos.provider.dart b/mobile/lib/providers/search/all_motion_photos.provider.dart deleted file mode 100644 index 48bc1bb80c..0000000000 --- a/mobile/lib/providers/search/all_motion_photos.provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final allMotionPhotosProvider = FutureProvider>((ref) async { - return ref.watch(assetServiceProvider).getMotionAssets(); -}); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart deleted file mode 100644 index 9a37d83320..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/services/search.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'paginated_search.provider.g.dart'; - -final paginatedSearchProvider = StateNotifierProvider( - (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), -); - -class PaginatedSearchNotifier extends StateNotifier { - final SearchService _searchService; - - PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1)); - - Future search(SearchFilter filter) async { - if (state.nextPage == null) { - return false; - } - - final result = await _searchService.search(filter, state.nextPage!); - - if (result == null) { - return false; - } - - state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage); - - return true; - } - - clear() { - state = const SearchResult(assets: [], nextPage: 1); - } -} - -@riverpod -Future paginatedSearchRenderList(Ref ref) { - final result = ref.watch(paginatedSearchProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(result.assets, GroupAssetsBy.none); -} diff --git a/mobile/lib/providers/search/paginated_search.provider.g.dart b/mobile/lib/providers/search/paginated_search.provider.g.dart deleted file mode 100644 index e984997967..0000000000 --- a/mobile/lib/providers/search/paginated_search.provider.g.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'paginated_search.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$paginatedSearchRenderListHash() => - r'22d715ff7864e5a946be38322ce7813616f899c2'; - -/// See also [paginatedSearchRenderList]. -@ProviderFor(paginatedSearchRenderList) -final paginatedSearchRenderListProvider = - AutoDisposeFutureProvider.internal( - paginatedSearchRenderList, - name: r'paginatedSearchRenderListProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$paginatedSearchRenderListHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 3ff8d67983..1f6f983154 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -1,9 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/person.service.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @@ -17,16 +14,6 @@ Future> getAllPeople(Ref ref) async { return people; } -@riverpod -Future personAssets(Ref ref, String personId) async { - final PersonService personService = ref.read(personServiceProvider); - final assets = await personService.getPersonAssets(personId); - - final settings = ref.read(appSettingsServiceProvider); - final groupBy = GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - return await RenderList.fromAssets(assets, groupBy); -} - @riverpod Future updatePersonName(Ref ref, String personId, String updatedName) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index 9595c36eec..23424c068f 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -24,7 +24,7 @@ final getAllPeopleProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; -String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; +String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; /// Copied from Dart SDK class _SystemHash { @@ -47,126 +47,6 @@ class _SystemHash { } } -/// See also [personAssets]. -@ProviderFor(personAssets) -const personAssetsProvider = PersonAssetsFamily(); - -/// See also [personAssets]. -class PersonAssetsFamily extends Family> { - /// See also [personAssets]. - const PersonAssetsFamily(); - - /// See also [personAssets]. - PersonAssetsProvider call(String personId) { - return PersonAssetsProvider(personId); - } - - @override - PersonAssetsProvider getProviderOverride( - covariant PersonAssetsProvider provider, - ) { - return call(provider.personId); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'personAssetsProvider'; -} - -/// See also [personAssets]. -class PersonAssetsProvider extends AutoDisposeFutureProvider { - /// See also [personAssets]. - PersonAssetsProvider(String personId) - : this._internal( - (ref) => personAssets(ref as PersonAssetsRef, personId), - from: personAssetsProvider, - name: r'personAssetsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$personAssetsHash, - dependencies: PersonAssetsFamily._dependencies, - allTransitiveDependencies: - PersonAssetsFamily._allTransitiveDependencies, - personId: personId, - ); - - PersonAssetsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.personId, - }) : super.internal(); - - final String personId; - - @override - Override overrideWith( - FutureOr Function(PersonAssetsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: PersonAssetsProvider._internal( - (ref) => create(ref as PersonAssetsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - personId: personId, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _PersonAssetsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is PersonAssetsProvider && other.personId == personId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, personId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin PersonAssetsRef on AutoDisposeFutureProviderRef { - /// The parameter `personId` of this provider. - String get personId; -} - -class _PersonAssetsProviderElement - extends AutoDisposeFutureProviderElement - with PersonAssetsRef { - _PersonAssetsProviderElement(super.provider); - - @override - String get personId => (origin as PersonAssetsProvider).personId; -} - -String _$updatePersonNameHash() => r'45f7693172de522a227406d8198811434cf2bbbc'; - /// See also [updatePersonName]. @ProviderFor(updatePersonName) const updatePersonNameProvider = UpdatePersonNameFamily(); diff --git a/mobile/lib/providers/search/recently_taken_asset.provider.dart b/mobile/lib/providers/search/recently_taken_asset.provider.dart deleted file mode 100644 index 157e7c2a74..0000000000 --- a/mobile/lib/providers/search/recently_taken_asset.provider.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; - -final recentlyTakenAssetProvider = FutureProvider>((ref) async { - final assetService = ref.read(assetServiceProvider); - - return assetService.getRecentlyTakenAssets(); -}); diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart deleted file mode 100644 index 71ea308dbf..0000000000 --- a/mobile/lib/providers/timeline.provider.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final singleUserTimelineProvider = StreamProvider.family((ref, userId) { - if (userId == null) { - return const Stream.empty(); - } - - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchHomeTimeline(userId); -}, dependencies: [localeProvider]); - -final multiUsersTimelineProvider = StreamProvider.family>((ref, userIds) { - ref.watch(localeProvider); - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchMultiUsersTimeline(userIds); -}, dependencies: [localeProvider]); - -final albumTimelineProvider = StreamProvider.autoDispose.family((ref, id) { - final album = ref.watch(albumWatcher(id)).value; - final timelineService = ref.watch(timelineServiceProvider); - - if (album != null) { - return timelineService.watchAlbumTimeline(album); - } - - return const Stream.empty(); -}); - -final archiveTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchArchiveTimeline(); -}); - -final favoriteTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchFavoriteTimeline(); -}); - -final trashTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchTrashTimeline(); -}); - -final allVideosTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAllVideosTimeline(); -}); - -final assetSelectionTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchAssetSelectionTimeline(); -}); - -final assetsTimelineProvider = FutureProvider.family>((ref, assets) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.getTimelineFromAssets(assets, null); -}); - -final lockedTimelineProvider = StreamProvider((ref) { - final timelineService = ref.watch(timelineServiceProvider); - return timelineService.watchLockedTimelineProvider(); -}); diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart deleted file mode 100644 index 41b9160b9b..0000000000 --- a/mobile/lib/providers/trash.provider.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/trash.service.dart'; -import 'package:logging/logging.dart'; - -class TrashNotifier extends StateNotifier { - final TrashService _trashService; - final _log = Logger('TrashNotifier'); - - TrashNotifier(this._trashService) : super(false); - - Future emptyTrash() async { - try { - await _trashService.emptyTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot empty trash", error, stack); - state = false; - } - } - - Future restoreAssets(Iterable assetList) async { - try { - await _trashService.restoreAssets(assetList); - return true; - } catch (error, stack) { - _log.severe("Cannot restore assets", error, stack); - return false; - } - } - - Future restoreTrash() async { - try { - await _trashService.restoreTrash(); - state = true; - } catch (error, stack) { - _log.severe("Cannot restore trash", error, stack); - state = false; - } - } -} - -final trashProvider = StateNotifierProvider((ref) { - return TrashNotifier(ref.watch(trashServiceProvider)); -}); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 10dcb2aff5..5a56b65793 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._userService) : super(null) { @@ -32,28 +30,3 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); - -class TimelineUserIdsProvider extends StateNotifier> { - TimelineUserIdsProvider(this._timelineService) : super([]) { - final listEquality = const ListEquality(); - _timelineService.getTimelineUserIds().then((users) => state = users); - streamSub = _timelineService.watchTimelineUserIds().listen((users) { - if (!listEquality.equals(state, users)) { - state = users; - } - }); - } - - late final StreamSubscription> streamSub; - final TimelineService _timelineService; - - @override - void dispose() { - streamSub.cancel(); - super.dispose(); - } -} - -final timelineUsersIdsProvider = StateNotifierProvider>((ref) { - return TimelineUserIdsProvider(ref.watch(timelineServiceProvider)); -}); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 6643404786..c79f40a25d 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -1,60 +1,27 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; -enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } - -class PendingChange { - final String id; - final PendingAction action; - final dynamic value; - - const PendingChange(this.id, this.action, this.value); - - @override - String toString() => 'PendingChange(id: $id, action: $action, value: $value)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is PendingChange && other.id == id && other.action == action; - } - - @override - int get hashCode => id.hashCode ^ action.hashCode; -} - class WebsocketState { final Socket? socket; final bool isConnected; - final List pendingChanges; - const WebsocketState({this.socket, required this.isConnected, required this.pendingChanges}); + const WebsocketState({this.socket, required this.isConnected}); - WebsocketState copyWith({Socket? socket, bool? isConnected, List? pendingChanges}) { - return WebsocketState( - socket: socket ?? this.socket, - isConnected: isConnected ?? this.isConnected, - pendingChanges: pendingChanges ?? this.pendingChanges, - ); + WebsocketState copyWith({Socket? socket, bool? isConnected}) { + return WebsocketState(socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected); } @override @@ -72,11 +39,10 @@ class WebsocketState { } class WebsocketNotifier extends StateNotifier { - WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false, pendingChanges: [])); + WebsocketNotifier(this._ref) : super(const WebsocketState(socket: null, isConnected: false)); final _log = Logger('WebsocketNotifier'); final Ref _ref; - final Debouncer _debounce = Debouncer(interval: const Duration(milliseconds: 500)); final Debouncer _batchDebouncer = Debouncer( interval: const Duration(seconds: 5), @@ -115,32 +81,21 @@ class WebsocketNotifier extends StateNotifier { socket.onConnect((_) { dPrint(() => "Established Websocket Connection"); - state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); + state = WebsocketState(isConnected: true, socket: socket); }); socket.onDisconnect((_) { dPrint(() => "Disconnect to Websocket Connection"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); socket.on('error', (errorMessage) { _log.severe("Websocket Error - $errorMessage"); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); }); - if (!Store.isBetaTimelineEnabled) { - socket.on('on_upload_success', _handleOnUploadSuccess); - socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleOnAssetTrash); - socket.on('on_asset_restore', _handleServerUpdates); - socket.on('on_asset_update', _handleServerUpdates); - socket.on('on_asset_stack_update', _handleServerUpdates); - socket.on('on_asset_hidden', _handleOnAssetHidden); - } else { - socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - + socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { @@ -155,109 +110,28 @@ class WebsocketNotifier extends StateNotifier { _batchedAssetUploadReady.clear(); state.socket?.dispose(); - state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); + state = const WebsocketState(isConnected: false, socket: null); } - void stopListenToEvent(String eventName) { - state.socket?.off(eventName); - } + Future waitForEvent(String event, bool Function(dynamic)? predicate, Duration timeout) { + final completer = Completer(); - void stopListenToOldEvents() { - state.socket?.off('on_upload_success'); - state.socket?.off('on_asset_delete'); - state.socket?.off('on_asset_trash'); - state.socket?.off('on_asset_restore'); - state.socket?.off('on_asset_update'); - state.socket?.off('on_asset_stack_update'); - state.socket?.off('on_asset_hidden'); - } - - void startListeningToOldEvents() { - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - state.socket?.on('on_asset_delete', _handleOnAssetDelete); - state.socket?.on('on_asset_trash', _handleOnAssetTrash); - state.socket?.on('on_asset_restore', _handleServerUpdates); - state.socket?.on('on_asset_update', _handleServerUpdates); - state.socket?.on('on_asset_stack_update', _handleServerUpdates); - state.socket?.on('on_asset_hidden', _handleOnAssetHidden); - } - - void stopListeningToBetaEvents() { - state.socket?.off('AssetUploadReadyV1'); - state.socket?.off('AssetEditReadyV1'); - } - - void startListeningToBetaEvents() { - state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); - state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady); - } - - void listenUploadEvent() { - dPrint(() => "Start listening to event on_upload_success"); - state.socket?.on('on_upload_success', _handleOnUploadSuccess); - } - - void addPendingChange(PendingAction action, dynamic value) { - final now = DateTime.now(); - state = state.copyWith( - pendingChanges: [...state.pendingChanges, PendingChange(now.millisecondsSinceEpoch.toString(), action, value)], - ); - _debounce.run(handlePendingChanges); - } - - Future _handlePendingTrashes() async { - final trashChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetTrash).toList(); - if (trashChanges.isNotEmpty) { - List remoteIds = trashChanges.expand((a) => (a.value as List).map((e) => e.toString())).toList(); - - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - await _ref.read(assetProvider.notifier).getAllAsset(); - - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => trashChanges.contains(c)).toList()); - } - } - - Future _handlePendingDeletes() async { - final deleteChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetDelete).toList(); - if (deleteChanges.isNotEmpty) { - List remoteIds = deleteChanges.map((a) => a.value.toString()).toList(); - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => deleteChanges.contains(c)).toList()); - } - } - - Future _handlePendingUploaded() async { - final uploadedChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetUploaded).toList(); - if (uploadedChanges.isNotEmpty) { - List remoteAssets = uploadedChanges.map((a) => AssetResponseDto.fromJson(a.value)).toList(); - for (final dto in remoteAssets) { - if (dto != null) { - final newAsset = Asset.remote(dto); - await _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); - } + void handler(dynamic data) { + if (predicate == null || predicate(data)) { + completer.complete(); + state.socket?.off(event, handler); } - state = state.copyWith( - pendingChanges: state.pendingChanges.whereNot((c) => uploadedChanges.contains(c)).toList(), - ); } - } - Future _handlingPendingHidden() async { - final hiddenChanges = state.pendingChanges.where((c) => c.action == PendingAction.assetHidden).toList(); - if (hiddenChanges.isNotEmpty) { - List remoteIds = hiddenChanges.map((a) => a.value.toString()).toList(); - final db = _ref.watch(dbProvider); - await db.writeTxn(() => db.assets.deleteAllByRemoteId(remoteIds)); + state.socket?.on(event, handler); - state = state.copyWith(pendingChanges: state.pendingChanges.whereNot((c) => hiddenChanges.contains(c)).toList()); - } - } - - Future handlePendingChanges() async { - await _handlePendingUploaded(); - await _handlePendingDeletes(); - await _handlingPendingHidden(); - await _handlePendingTrashes(); + return completer.future.timeout( + timeout, + onTimeout: () { + state.socket?.off(event, handler); + completer.completeError(TimeoutException("Timeout waiting for event: $event")); + }, + ); } void _handleOnConfigUpdate(dynamic _) { @@ -265,21 +139,6 @@ class WebsocketNotifier extends StateNotifier { _ref.read(serverInfoProvider.notifier).getServerConfig(); } - // Refresh updated assets - void _handleServerUpdates(dynamic _) { - _ref.read(assetProvider.notifier).getAllAsset(); - } - - void _handleOnUploadSuccess(dynamic data) => addPendingChange(PendingAction.assetUploaded, data); - - void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data); - - void _handleOnAssetTrash(dynamic data) { - addPendingChange(PendingAction.assetTrash, data); - } - - void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); - _handleReleaseUpdates(dynamic data) { // Json guard if (data is! Map) { diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart deleted file mode 100644 index 2d24004944..0000000000 --- a/mobile/lib/repositories/album.repository.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AlbumSort { remoteId, localId } - -final albumRepositoryProvider = Provider((ref) => AlbumRepository(ref.watch(dbProvider))); - -class AlbumRepository extends DatabaseRepository { - const AlbumRepository(super.db); - - Future count({bool? local}) { - final baseQuery = db.albums.where(); - final QueryBuilder query = switch (local) { - null => baseQuery.noOp(), - true => baseQuery.localIdIsNotNull(), - false => baseQuery.remoteIdIsNotNull(), - }; - return query.count(); - } - - Future create(Album album) => txn(() => db.albums.store(album)); - - Future getByName(String name, {bool? shared, bool? remote, bool? owner}) { - var query = db.albums.filter().nameEqualTo(name); - if (shared != null) { - query = query.sharedEqualTo(shared); - } - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - if (owner == true) { - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - } else if (owner == false) { - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - } - if (remote == true) { - query = query.localIdIsNull(); - } else if (remote == false) { - query = query.remoteIdIsNull(); - } - return query.findFirst(); - } - - Future update(Album album) => txn(() => db.albums.store(album)); - - Future delete(int albumId) => txn(() => db.albums.delete(albumId)); - - Future> getAll({bool? shared, bool? remote, int? ownerId, AlbumSort? sortBy}) { - final baseQuery = db.albums.where(); - final QueryBuilder afterWhere; - if (remote == null) { - afterWhere = baseQuery.noOp(); - } else if (remote) { - afterWhere = baseQuery.remoteIdIsNotNull(); - } else { - afterWhere = baseQuery.localIdIsNotNull(); - } - QueryBuilder filterQuery = afterWhere.filter().noOp(); - if (shared != null) { - filterQuery = filterQuery.sharedEqualTo(true); - } - if (ownerId != null) { - filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId)); - } - final QueryBuilder query = switch (sortBy) { - null => filterQuery.noOp(), - AlbumSort.remoteId => filterQuery.sortByRemoteId(), - AlbumSort.localId => filterQuery.sortByLocalId(), - }; - return query.findAll(); - } - - Future get(int id) => db.albums.get(id); - - Future getByRemoteId(String remoteId) { - return db.albums.filter().remoteIdEqualTo(remoteId).findFirst(); - } - - Future removeUsers(Album album, List users) => - txn(() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto))); - - Future addAssets(Album album, List assets) => txn(() => album.assets.update(link: assets)); - - Future removeAssets(Album album, List assets) => txn(() => album.assets.update(unlink: assets)); - - Future recalculateMetadata(Album album) async { - album.startDate = await album.assets.filter().fileCreatedAtProperty().min(); - album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); - album.lastModifiedAssetTimestamp = await album.assets.filter().updatedAtProperty().max(); - return album; - } - - Future addUsers(Album album, List users) => - txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto))); - - Future deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll()); - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - var query = db.albums.filter().nameContains(searchTerm, caseSensitive: false).remoteIdIsNotNull(); - final isarUserId = fastHash(Store.get(StoreKey.currentUser).id); - - switch (filterMode) { - case QuickFilterMode.sharedWithMe: - query = query.owner((q) => q.not().isarIdEqualTo(isarUserId)); - case QuickFilterMode.myAlbums: - query = query.owner((q) => q.isarIdEqualTo(isarUserId)); - case QuickFilterMode.all: - break; - } - - return await query.findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.albums.clear(); - }); - } - - Stream> watchRemoteAlbums() { - return db.albums.where().remoteIdIsNotNull().watch(); - } - - Stream> watchLocalAlbums() { - return db.albums.where().localIdIsNotNull().watch(); - } - - Stream watchAlbum(int id) { - return db.albums.watchObject(id, fireImmediately: true); - } -} diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart deleted file mode 100644 index 525f0906ba..0000000000 --- a/mobile/lib/repositories/album_api.repository.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart' show AlbumAssetOrder, RemoteAlbum; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/api.repository.dart'; -import 'package:openapi/api.dart'; - -final albumApiRepositoryProvider = Provider((ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi)); - -class AlbumApiRepository extends ApiRepository { - final AlbumsApi _api; - - AlbumApiRepository(this._api); - - Future get(String id) async { - final dto = await checkNull(_api.getAlbumInfo(id)); - return _toAlbum(dto); - } - - Future> getAll({bool? shared}) async { - final dtos = await checkNull(_api.getAllAlbums(shared: shared)); - return dtos.map(_toAlbum).toList(); - } - - Future create( - String name, { - required Iterable assetIds, - Iterable sharedUserIds = const [], - String? description, - }) async { - final users = sharedUserIds.map((id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor)); - final responseDto = await checkNull( - _api.createAlbum( - CreateAlbumDto( - albumName: name, - description: description, - assetIds: assetIds.toList(), - albumUsers: users.toList(), - ), - ), - ); - return _toAlbum(responseDto); - } - - // TODO: Change name after removing old method - Future createDriftAlbum(String name, {required Iterable assetIds, String? description}) async { - final responseDto = await checkNull( - _api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())), - ); - - return _toRemoteAlbum(responseDto); - } - - Future update( - String albumId, { - String? name, - String? thumbnailAssetId, - String? description, - bool? activityEnabled, - SortOrder? sortOrder, - }) async { - AssetOrder? order; - if (sortOrder != null) { - order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc; - } - - final response = await checkNull( - _api.updateAlbumInfo( - albumId, - UpdateAlbumDto( - albumName: name, - albumThumbnailAssetId: thumbnailAssetId, - description: description, - isActivityEnabled: activityEnabled, - order: order, - ), - ), - ); - - return _toAlbum(response); - } - - Future delete(String albumId) { - return _api.deleteAlbum(albumId); - } - - Future<({List added, List duplicates})> addAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - - final List added = []; - final List duplicates = []; - - for (final result in response) { - if (result.success) { - added.add(result.id); - } else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) { - duplicates.add(result.id); - } - } - return (added: added, duplicates: duplicates); - } - - Future<({List removed, List failed})> removeAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List removed = [], failed = []; - for (final dto in response) { - if (dto.success) { - removed.add(dto.id); - } else { - failed.add(dto.id); - } - } - return (removed: removed, failed: failed); - } - - Future addUsers(String albumId, Iterable userIds) async { - final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); - final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers))); - return _toAlbum(response); - } - - Future removeUser(String albumId, {required String userId}) { - return _api.removeUserFromAlbum(albumId, userId); - } - - static Album _toAlbum(AlbumResponseDto dto) { - final Album album = Album( - remoteId: dto.id, - name: dto.albumName, - createdAt: dto.createdAt, - modifiedAt: dto.updatedAt, - lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, - shared: dto.shared, - startDate: dto.startDate, - description: dto.description, - endDate: dto.endDate, - activityEnabled: dto.isActivityEnabled, - sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, - ); - album.remoteAssetCount = dto.assetCount; - album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner)); - album.remoteThumbnailAssetId = dto.albumThumbnailAssetId; - final users = dto.albumUsers.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user)); - album.sharedUsers.addAll(users.map(entity.User.fromDto)); - final assets = dto.assets.map(Asset.remote).toList(); - album.assets.addAll(assets); - - return album; - } - - static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) { - return RemoteAlbum( - id: dto.id, - name: dto.albumName, - ownerId: dto.owner.id, - description: dto.description, - createdAt: dto.createdAt, - updatedAt: dto.updatedAt, - thumbnailAssetId: dto.albumThumbnailAssetId, - isActivityEnabled: dto.isActivityEnabled, - order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, - assetCount: dto.assetCount, - ownerName: dto.owner.name, - isShared: dto.albumUsers.length > 2, - ); - } -} diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart deleted file mode 100644 index 89860f4e75..0000000000 --- a/mobile/lib/repositories/album_media.repository.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:photo_manager/photo_manager.dart' hide AssetType; - -final albumMediaRepositoryProvider = Provider((ref) => const AlbumMediaRepository()); - -class AlbumMediaRepository { - const AlbumMediaRepository(); - - bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, true); - - FilterOptionGroup? _getAlbumFilter({ - DateTimeCond? updateTimeCond, - bool? containsPathModified, - List? orderBy, - }) => useCustomFilter - ? FilterOptionGroup( - imageOption: const FilterOption(needTitle: true, sizeConstraint: SizeConstraint(ignoreSize: true)), - videoOption: const FilterOption( - needTitle: true, - sizeConstraint: SizeConstraint(ignoreSize: true), - durationConstraint: DurationConstraint(allowNullable: true), - ), - containsPathModified: containsPathModified ?? false, - createTimeCond: DateTimeCond.def().copyWith(ignore: true), - updateTimeCond: updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), - orders: orderBy ?? [], - ) - : null; - - Future> getAll() async { - final filter = useCustomFilter - ? CustomFilter.sql(where: '${CustomColumns.base.width} > 0') - : FilterOptionGroup(containsPathModified: true); - - final List assetPathEntities = await PhotoManager.getAssetPathList( - hasAll: true, - filterOption: filter, - ); - return assetPathEntities.map(_toAlbum).toList(); - } - - Future> getAssetIds(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - final List assets = await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff); - return assets.map((e) => e.id).toList(); - } - - Future getAssetCount(String albumId) async { - final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); - return album.assetCountAsync; - } - - Future> getAssets( - String albumId, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - bool orderByModificationDate = false, - }) async { - final onDevice = await AssetPathEntity.fromId( - albumId, - filterOption: _getAlbumFilter( - updateTimeCond: modifiedFrom == null && modifiedUntil == null - ? null - : DateTimeCond(min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760)), - orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], - ), - ); - - final List assets = await onDevice.getAssetListRange(start: start, end: end); - return assets.map(AssetMediaRepository.toAsset).toList().cast(); - } - - Future get(String id) async { - final assetPathEntity = await AssetPathEntity.fromId(id, filterOption: _getAlbumFilter(containsPathModified: true)); - return _toAlbum(assetPathEntity); - } - - static Album _toAlbum(AssetPathEntity assetPathEntity) { - final Album album = Album( - name: assetPathEntity.name, - createdAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - modifiedAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(), - shared: false, - activityEnabled: false, - ); - album.owner.value = User.fromDto(Store.get(StoreKey.currentUser)); - album.localId = assetPathEntity.id; - album.isAll = assetPathEntity.isAll; - return album; - } -} diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart deleted file mode 100644 index 79af8b4921..0000000000 --- a/mobile/lib/repositories/asset.repository.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:isar/isar.dart'; - -enum AssetSort { checksum, ownerIdChecksum } - -final assetRepositoryProvider = Provider((ref) => AssetRepository(ref.watch(dbProvider))); - -class AssetRepository extends DatabaseRepository { - const AssetRepository(super.db); - - Future> getByAlbum( - Album album, { - Iterable notOwnedBy = const [], - String? ownerId, - AssetState? state, - AssetSort? sortBy, - }) { - var query = album.assets.filter(); - final isarUserIds = notOwnedBy.map(fastHash).toList(); - if (notOwnedBy.length == 1) { - query = query.not().ownerIdEqualTo(isarUserIds.first); - } else if (notOwnedBy.isNotEmpty) { - query = query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id)); - } - if (ownerId != null) { - query = query.ownerIdEqualTo(fastHash(ownerId)); - } - - if (state != null) { - query = switch (state) { - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotNull().remoteIdIsNotNull(), - }; - } - - final QueryBuilder sortedQuery = switch (sortBy) { - null => query.noOp(), - AssetSort.checksum => query.sortByChecksum(), - AssetSort.ownerIdChecksum => query.sortByOwnerId().thenByChecksum(), - }; - - return sortedQuery.findAll(); - } - - Future deleteByIds(List ids) => txn(() async { - await db.assets.deleteAll(ids); - await db.exifInfos.deleteAll(ids); - }); - - Future getByRemoteId(String id) => db.assets.getByRemoteId(id); - - Future> getAllByRemoteId(Iterable ids, {AssetState? state}) async { - if (ids.isEmpty) { - return []; - } - - return _getAllByRemoteIdImpl(ids, state).findAll(); - } - - QueryBuilder _getAllByRemoteIdImpl(Iterable ids, AssetState? state) { - final query = db.assets.remote(ids).filter(); - return switch (state) { - null => query.noOp(), - AssetState.local => query.remoteIdIsNull(), - AssetState.remote => query.localIdIsNull(), - AssetState.merged => query.localIdIsNotEmpty().remoteIdIsNotNull(), - }; - } - - Future> getAll({required String ownerId, AssetState? state, AssetSort? sortBy, int? limit}) { - final baseQuery = db.assets.where(); - final isarUserIds = fastHash(ownerId); - final QueryBuilder filteredQuery = switch (state) { - null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull().ownerIdEqualTo(isarUserIds), - AssetState.merged => - baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).filter().remoteIdIsNotNull().localIdIsNotNull(), - }; - - final QueryBuilder query = switch (sortBy) { - null => filteredQuery.noOp(), - AssetSort.checksum => filteredQuery.sortByChecksum(), - AssetSort.ownerIdChecksum => filteredQuery.sortByOwnerId().thenByChecksum(), - }; - - return limit == null ? query.findAll() : query.limit(limit).findAll(); - } - - Future> updateAll(List assets) async { - await txn(() => db.assets.putAll(assets)); - return assets; - } - - Future> getMatches({ - required List assets, - required String ownerId, - AssetState? state, - int limit = 100, - }) { - final baseQuery = db.assets.where(); - final QueryBuilder query = switch (state) { - null => baseQuery.noOp(), - AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull(), - AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull(), - AssetState.merged => baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(), - }; - return _getMatchesImpl(query, fastHash(ownerId), assets, limit); - } - - Future update(Asset asset) async { - await txn(() => asset.put(db)); - return asset; - } - - Future upsertDuplicatedAssets(Iterable duplicatedAssets) => - txn(() => db.duplicatedAssets.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList())); - - Future> getAllDuplicatedAssetIds() => db.duplicatedAssets.where().idProperty().findAll(); - - Future getByOwnerIdChecksum(int ownerId, String checksum) => - db.assets.getByOwnerIdChecksum(ownerId, checksum); - - Future> getAllByOwnerIdChecksum(List ownerIds, List checksums) => - db.assets.getAllByOwnerIdChecksum(ownerIds, checksums); - - Future> getAllLocal() => db.assets.where().localIdIsNotNull().findAll(); - - Future deleteAllByRemoteId(List ids, {AssetState? state}) => - txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll()); - - Future> getStackAssets(String stackId) { - return db.assets - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackIdEqualTo(stackId) - // orders primary asset first as its ID is null - .sortByStackPrimaryAssetId() - .thenByFileCreatedAtDesc() - .findAll(); - } - - Future clearTable() async { - await txn(() async { - await db.assets.clear(); - }); - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return db.assets.watchObject(id, fireImmediately: fireImmediately); - } - - Future> getTrashAssets(String userId) { - return db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .isTrashedEqualTo(true) - .findAll(); - } - - Future> getRecentlyTakenAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc() - .findAll(); - } - - Future> getMotionAssets(String userId) { - return db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .livePhotoVideoIdIsNotNull() - .findAll(); - } -} - -Future> _getMatchesImpl( - QueryBuilder query, - int ownerId, - List assets, - int limit, -) => query - .ownerIdEqualTo(ownerId) - .anyOf( - assets, - (q, Asset a) => q - .fileNameEqualTo(a.fileName) - .and() - .durationInSecondsEqualTo(a.durationInSeconds) - .and() - .fileCreatedAtBetween( - a.fileCreatedAt.subtract(const Duration(hours: 12)), - a.fileCreatedAt.add(const Duration(hours: 12)), - ) - .and() - .not() - .checksumEqualTo(a.checksum), - ) - .sortByFileName() - .thenByFileCreatedAt() - .thenByFileModifiedAt() - .limit(limit) - .findAll(); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 011b1edc94..2943177d60 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,8 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction; import 'package:immich_mobile/domain/models/stack.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -11,7 +11,6 @@ import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( ref.watch(apiServiceProvider).assetsApi, - ref.watch(apiServiceProvider).searchApi, ref.watch(apiServiceProvider).stacksApi, ref.watch(apiServiceProvider).trashApi, ), @@ -19,32 +18,10 @@ final assetApiRepositoryProvider = Provider( class AssetApiRepository extends ApiRepository { final AssetsApi _api; - final SearchApi _searchApi; final StacksApi _stacksApi; final TrashApi _trashApi; - AssetApiRepository(this._api, this._searchApi, this._stacksApi, this._trashApi); - - Future update(String id, {String? description}) async { - final response = await checkNull(_api.updateAsset(id, UpdateAssetDto(description: description))); - return Asset.remote(response); - } - - Future> search({List personIds = const []}) async { - // TODO this always fetches all assets, change API and usage to actually do pagination - final List result = []; - bool hasNext = true; - int currentPage = 1; - while (hasNext) { - final response = await checkNull( - _searchApi.searchAssets(MetadataSearchDto(personIds: personIds, page: currentPage, size: 1000)), - ); - result.addAll(response.assets.items.map(Asset.remote)); - hasNext = response.assets.nextPage != null; - currentPage++; - } - return result; - } + AssetApiRepository(this._api, this._stacksApi, this._trashApi); Future delete(List ids, bool force) async { return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force)); @@ -105,6 +82,14 @@ class AssetApiRepository extends ApiRepository { Future updateRating(String assetId, int rating) { return _api.updateAsset(assetId, UpdateAssetDto(rating: rating)); } + + Future editAsset(String assetId, List edits) { + return _api.editAsset(assetId, AssetEditsCreateDto(edits: edits.map((e) => e.toApi()).toList())); + } + + Future removeEdits(String assetId) async { + return _api.removeAssetEdits(assetId); + } } extension on StackResponseDto { @@ -112,3 +97,22 @@ extension on StackResponseDto { return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList()); } } + +extension on AssetEdit { + AssetEditActionItemDto toApi() { + return switch (this) { + CropEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.crop, + parameters: parameters.toJson(), + ), + RotateEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.rotate, + parameters: parameters.toJson(), + ), + MirrorEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.mirror, + parameters: parameters.toJson(), + ), + }; + } +} diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index fecfe6df4d..a2d8bfe162 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -5,15 +5,10 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -50,39 +45,9 @@ class AssetMediaRepository { return PhotoManager.editor.deleteWithIds(ids); } - Future get(String id) async { + Future get(String id) async { final entity = await AssetEntity.fromId(id); - return toAsset(entity); - } - - static asset_entity.Asset? toAsset(AssetEntity? local) { - if (local == null) return null; - - final asset_entity.Asset asset = asset_entity.Asset( - checksum: "", - localId: local.id, - ownerId: fastHash(Store.get(StoreKey.currentUser).id), - fileCreatedAt: local.createDateTime, - fileModifiedAt: local.modifiedDateTime, - updatedAt: local.modifiedDateTime, - durationInSeconds: local.duration, - type: asset_entity.AssetType.values[local.typeInt], - fileName: local.title!, - width: local.width, - height: local.height, - isFavorite: local.isFavorite, - ); - - if (asset.fileCreatedAt.year == 1970) { - asset.fileCreatedAt = asset.fileModifiedAt; - } - - if (local.latitude != null) { - asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude); - } - - asset.local = local; - return asset; + return entity; } Future getOriginalFilename(String id) async { diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index a8544ef6c0..c16b728ae5 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -2,40 +2,21 @@ import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(dbProvider), ref.watch(driftProvider)), -); +final authRepositoryProvider = Provider((ref) => AuthRepository(ref.watch(driftProvider))); -class AuthRepository extends DatabaseRepository { +class AuthRepository { final Drift _drift; - const AuthRepository(super.db, this._drift); + const AuthRepository(this._drift); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); - - return db.writeTxn(() { - return Future.wait([ - db.assets.clear(), - db.exifInfos.clear(), - db.albums.clear(), - db.eTags.clear(), - db.users.clear(), - ]); - }); } bool getEndpointSwitchingFeature() { diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart deleted file mode 100644 index 6cee6a4427..0000000000 --- a/mobile/lib/repositories/backup.repository.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -enum BackupAlbumSort { id } - -final backupAlbumRepositoryProvider = Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); - -class BackupAlbumRepository extends DatabaseRepository { - const BackupAlbumRepository(super.db); - - Future> getAll({BackupAlbumSort? sort}) { - final baseQuery = db.backupAlbums.where(); - final QueryBuilder query = switch (sort) { - null => baseQuery.noOp(), - BackupAlbumSort.id => baseQuery.sortById(), - }; - return query.findAll(); - } - - Future> getIdsBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); - - Future> getAllBySelection(BackupSelection backup) => - db.backupAlbums.filter().selectionEqualTo(backup).findAll(); - - Future deleteAll(List ids) => txn(() => db.backupAlbums.deleteAll(ids)); - - Future updateAll(List backupAlbums) => txn(() => db.backupAlbums.putAll(backupAlbums)); -} diff --git a/mobile/lib/repositories/database.repository.dart b/mobile/lib/repositories/database.repository.dart deleted file mode 100644 index 71c15e1c40..0000000000 --- a/mobile/lib/repositories/database.repository.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; -import 'package:immich_mobile/interfaces/database.interface.dart'; -import 'package:isar/isar.dart'; - -/// copied from Isar; needed to check if an async transaction is already active -const Symbol _zoneTxn = #zoneTxn; - -abstract class DatabaseRepository implements IDatabaseRepository { - final Isar db; - const DatabaseRepository(this.db); - - bool get inTxn => Zone.current[_zoneTxn] != null; - - Future txn(Future Function() callback) => inTxn ? callback() : transaction(callback); - - @override - Future transaction(Future Function() callback) => db.writeTxn(callback); -} - -extension Asd on QueryBuilder { - QueryBuilder noOp() { - // ignore: invalid_use_of_protected_member - return QueryBuilder.apply(this, (query) => query); - } -} diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart deleted file mode 100644 index 768d95b95c..0000000000 --- a/mobile/lib/repositories/etag.repository.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final etagRepositoryProvider = Provider((ref) => ETagRepository(ref.watch(dbProvider))); - -class ETagRepository extends DatabaseRepository { - const ETagRepository(super.db); - - Future> getAllIds() => db.eTags.where().idProperty().findAll(); - - Future get(String id) => db.eTags.getById(id); - - Future upsertAll(List etags) => txn(() => db.eTags.putAll(etags)); - - Future deleteByIds(List ids) => txn(() => db.eTags.deleteAllById(ids)); - - Future getById(String id) => db.eTags.getById(id); - - Future clearTable() async { - await txn(() async { - await db.eTags.clear(); - }); - } -} diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index f5cdb6d5c0..c54813a757 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -3,18 +3,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository()); class FileMediaRepository { const FileMediaRepository(); - Future saveImage(Uint8List data, {required String title, String? relativePath}) async { - final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); - } Future saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); @@ -30,24 +24,18 @@ class FileMediaRepository { ); } - Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { + Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveLivePhoto({required File image, required File video, required String title}) async { + Future saveLivePhoto({required File image, required File video, required String title}) async { final entity = await PhotoManager.editor.darwin.saveLivePhoto(imageFile: image, videoFile: video, title: title); - return AssetMediaRepository.toAsset(entity); + return entity; } - Future saveVideo(File file, {required String title, String? relativePath}) async { + Future saveVideo(File file, {required String title, String? relativePath}) async { final entity = await PhotoManager.editor.saveVideo(file, title: title, relativePath: relativePath); - return AssetMediaRepository.toAsset(entity); + return entity; } - - Future clearFileCache() => PhotoManager.clearFileCache(); - - Future enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true); - - Future requestExtendedPermissions() => PhotoManager.requestPermissionExtend(); } diff --git a/mobile/lib/repositories/folder_api.repository.dart b/mobile/lib/repositories/folder_api.repository.dart index d20ca8e0a9..8c9959389c 100644 --- a/mobile/lib/repositories/folder_api.repository.dart +++ b/mobile/lib/repositories/folder_api.repository.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:logging/logging.dart'; @@ -23,10 +24,10 @@ class FolderApiRepository extends ApiRepository { } } - Future> getAssetsForPath(String? path) async { + Future> getAssetsForPath(String? path) async { try { final list = await _api.getAssetsByOriginalPath(path ?? '/'); - return list != null ? list.map(Asset.remote).toList() : []; + return list != null ? list.map((e) => e.toDtoWithExif()).toList() : []; } catch (e, stack) { _log.severe("Failed to fetch Assets by original path", e, stack); return []; diff --git a/mobile/lib/repositories/partner.repository.dart b/mobile/lib/repositories/partner.repository.dart deleted file mode 100644 index 7f5ce62e0c..0000000000 --- a/mobile/lib/repositories/partner.repository.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:isar/isar.dart'; - -final partnerRepositoryProvider = Provider((ref) => PartnerRepository(ref.watch(dbProvider))); - -class PartnerRepository extends DatabaseRepository { - const PartnerRepository(super.db); - - Future> getSharedBy() async { - return (await db.users.filter().isPartnerSharedByEqualTo(true).sortById().findAll()).map((u) => u.toDto()).toList(); - } - - Future> getSharedWith() async { - return (await db.users.filter().isPartnerSharedWithEqualTo(true).sortById().findAll()) - .map((u) => u.toDto()) - .toList(); - } - - Stream> watchSharedBy() { - return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } - - Stream> watchSharedWith() { - return (db.users.filter().isPartnerSharedWithEqualTo(true).sortById().watch()).map( - (users) => users.map((u) => u.toDto()).toList(), - ); - } -} diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart index d497da4d4c..69b6740cbe 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -21,8 +21,8 @@ class PartnerApiRepository extends ApiRepository { return response.map(UserConverter.fromPartnerDto).toList(); } - Future create(String id) async { - final dto = await checkNull(_api.createPartnerDeprecated(id)); + Future create(String sharedWithId) async { + final dto = await checkNull(_api.createPartner(PartnerCreateDto(sharedWithId: sharedWithId))); return UserConverter.fromPartnerDto(dto); } diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart deleted file mode 100644 index c8c173b6f6..0000000000 --- a/mobile/lib/repositories/timeline.repository.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/repositories/database.repository.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:isar/isar.dart'; - -final timelineRepositoryProvider = Provider((ref) => TimelineRepository(ref.watch(dbProvider))); - -class TimelineRepository extends DatabaseRepository { - const TimelineRepository(super.db); - - Future> getTimelineUserIds(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().findAll(); - } - - Stream> watchTimelineUsers(String id) { - return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().watch(); - } - - Stream watchArchiveTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.archive) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchFavoriteTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isFavoriteEqualTo(true) - .not() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAlbumTimeline(Album album, GroupAssetsBy groupAssetByOption) { - final query = album.assets.filter().isTrashedEqualTo(false).not().visibilityEqualTo(AssetVisibilityEnum.locked); - - final withSortedOption = switch (album.sortOrder) { - SortOrder.asc => query.sortByFileCreatedAt(), - SortOrder.desc => query.sortByFileCreatedAtDesc(), - }; - - return _watchRenderList(withSortedOption, groupAssetByOption); - } - - Stream watchTrashTimeline(String userId) { - final query = db.assets.filter().ownerIdEqualTo(fastHash(userId)).isTrashedEqualTo(true).sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchAllVideosTimeline(String userId) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .typeEqualTo(AssetType.video) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchHomeTimeline(String userId, GroupAssetsBy groupAssetByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, groupAssetByOption); - } - - Stream watchMultiUsersTimeline(List userIds, GroupAssetsBy groupAssetByOption) { - final isarUserIds = userIds.map(fastHash).toList(); - final query = db.assets - .where() - .anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id)) - .filter() - .isTrashedEqualTo(false) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - return _watchRenderList(query, groupAssetByOption); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy getGroupByOption) { - return RenderList.fromAssets(assets, getGroupByOption); - } - - Stream watchAssetSelectionTimeline(String userId) { - final query = db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(fastHash(userId)) - .visibilityEqualTo(AssetVisibilityEnum.timeline) - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, GroupAssetsBy.none); - } - - Stream watchLockedTimeline(String userId, GroupAssetsBy getGroupByOption) { - final query = db.assets - .where() - .ownerIdEqualToAnyChecksum(fastHash(userId)) - .filter() - .visibilityEqualTo(AssetVisibilityEnum.locked) - .isTrashedEqualTo(false) - .sortByFileCreatedAtDesc(); - - return _watchRenderList(query, getGroupByOption); - } - - Stream _watchRenderList( - QueryBuilder query, - GroupAssetsBy groupAssetsBy, - ) async* { - yield await RenderList.fromQuery(query, groupAssetsBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupAssetsBy); - } - } -} diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index b05a28172d..b6b08d7831 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -19,7 +19,6 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { - _handleLockedViewState(route, previousRoute); _handleDriftLockedFolderState(route, previousRoute); Future(() { ref.read(currentRouteNameProvider.notifier).state = route.settings.name; @@ -28,21 +27,6 @@ class AppNavigationObserver extends AutoRouterObserver { }); } - _handleLockedViewState(Route route, Route? previousRoute) { - final isInLockedView = ref.read(inLockedViewProvider); - final isFromLockedViewToDetailView = - route.settings.name == GalleryViewerRoute.name && previousRoute?.settings.name == LockedRoute.name; - - final isFromDetailViewToInfoPanelView = - route.settings.name == null && previousRoute?.settings.name == GalleryViewerRoute.name && isInLockedView; - - if (route.settings.name == LockedRoute.name || isFromLockedViewToDetailView || isFromDetailViewToInfoPanelView) { - Future(() => ref.read(inLockedViewProvider.notifier).state = true); - } else { - Future(() => ref.read(inLockedViewProvider.notifier).state = false); - } - } - _handleDriftLockedFolderState(Route route, Route? previousRoute) { final isInLockedView = ref.read(inLockedViewProvider); final isFromLockedViewToDetailView = diff --git a/mobile/lib/routing/backup_permission_guard.dart b/mobile/lib/routing/backup_permission_guard.dart deleted file mode 100644 index f52516f2e5..0000000000 --- a/mobile/lib/routing/backup_permission_guard.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/providers/gallery_permission.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class BackupPermissionGuard extends AutoRouteGuard { - final GalleryPermissionNotifier _permission; - - const BackupPermissionGuard(this._permission); - - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final p = _permission.hasPermission; - if (p) { - resolver.next(true); - } else { - unawaited(router.push(const PermissionOnboardingRoute())); - } - } -} diff --git a/mobile/lib/routing/gallery_guard.dart b/mobile/lib/routing/gallery_guard.dart deleted file mode 100644 index 6a4b1bddab..0000000000 --- a/mobile/lib/routing/gallery_guard.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/routing/router.dart'; - -/// Handles duplicate navigation to this route (primarily for deep linking) -class GalleryGuard extends AutoRouteGuard { - const GalleryGuard(); - @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { - final newRouteName = resolver.route.name; - final currentTopRouteName = router.stack.isNotEmpty ? router.stack.last.name : null; - - if (currentTopRouteName == newRouteName) { - // Replace instead of pushing duplicate - final args = resolver.route.args as GalleryViewerRouteArgs; - - unawaited( - router.replace( - GalleryViewerRoute( - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ), - ), - ); - // Prevent further navigation since we replaced the route - resolver.next(false); - return; - } - resolver.next(true); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index b385bcbf71..9c539a37a6 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -4,78 +4,34 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; -import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_asset_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_options.page.dart'; -import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'; -import 'package:immich_mobile/pages/album/album_viewer.page.dart'; -import 'package:immich_mobile/pages/albums/albums.page.dart'; -import 'package:immich_mobile/pages/backup/album_preview.page.dart'; -import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; -import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; -import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_asset_detail.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; -import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; -import 'package:immich_mobile/pages/common/change_experience.page.dart'; -import 'package:immich_mobile/pages/common/create_album.page.dart'; -import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; -import 'package:immich_mobile/pages/common/tab_controller.page.dart'; import 'package:immich_mobile/pages/common/tab_shell.page.dart'; -import 'package:immich_mobile/pages/editing/crop.page.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/pages/editing/filter.page.dart'; -import 'package:immich_mobile/pages/library/archive.page.dart'; -import 'package:immich_mobile/pages/library/favorite.page.dart'; import 'package:immich_mobile/pages/library/folder/folder.page.dart'; -import 'package:immich_mobile/pages/library/library.page.dart'; -import 'package:immich_mobile/pages/library/local_albums.page.dart'; -import 'package:immich_mobile/pages/library/locked/locked.page.dart'; import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; -import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; -import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; -import 'package:immich_mobile/pages/library/trash.page.dart'; import 'package:immich_mobile/pages/login/change_password.page.dart'; import 'package:immich_mobile/pages/login/login.page.dart'; -import 'package:immich_mobile/pages/onboarding/permission_onboarding.page.dart'; -import 'package:immich_mobile/pages/photos/memory.page.dart'; -import 'package:immich_mobile/pages/photos/photos.page.dart'; -import 'package:immich_mobile/pages/search/all_motion_videos.page.dart'; -import 'package:immich_mobile/pages/search/all_people.page.dart'; -import 'package:immich_mobile/pages/search/all_places.page.dart'; -import 'package:immich_mobile/pages/search/all_videos.page.dart'; -import 'package:immich_mobile/pages/search/map/map.page.dart'; import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; -import 'package:immich_mobile/pages/search/person_result.page.dart'; -import 'package:immich_mobile/pages/search/recently_taken.page.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; @@ -105,25 +61,19 @@ import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; -import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart'; +import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; -import 'package:immich_mobile/routing/backup_permission_guard.dart'; -import 'package:immich_mobile/routing/custom_transition_builders.dart'; import 'package:immich_mobile/routing/duplicate_guard.dart'; -import 'package:immich_mobile/routing/gallery_guard.dart'; import 'package:immich_mobile/routing/locked_guard.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; @@ -141,9 +91,7 @@ final appRouterProvider = Provider( class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; - late final BackupPermissionGuard _backupPermissionGuard; late final LockedGuard _lockedGuard; - late final GalleryGuard _galleryGuard; AppRouter( ApiService apiService, @@ -154,8 +102,6 @@ class AppRouter extends RootStackRouter { _authGuard = AuthGuard(apiService); _duplicateGuard = const DuplicateGuard(); _lockedGuard = LockedGuard(apiService, secureStorageService, localAuthService); - _backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier); - _galleryGuard = const GalleryGuard(); } @override @@ -164,20 +110,8 @@ class AppRouter extends RootStackRouter { @override late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), - AutoRoute(page: PermissionOnboardingRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute( - page: TabControllerRoute.page, - guards: [_authGuard, _duplicateGuard], - children: [ - AutoRoute(page: PhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), - AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), - ], - ), AutoRoute( page: TabShellRoute.page, guards: [_authGuard, _duplicateGuard], @@ -188,105 +122,17 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), ], ), - CustomRoute( - page: GalleryViewerRoute.page, - guards: [_authGuard, _galleryGuard], - transitionsBuilder: CustomTransitionsBuilders.zoomedPage, - ), - AutoRoute(page: BackupControllerRoute.page, guards: [_authGuard, _duplicateGuard, _backupPermissionGuard]), - AutoRoute(page: AllPlacesRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: EditImageRoute.page), - AutoRoute(page: CropImageRoute.page), - AutoRoute(page: FilterImageRoute.page), AutoRoute(page: ProfilePictureCropRoute.page), - CustomRoute( - page: FavoritesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllMotionPhotosRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: RecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAssetSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - CustomRoute( - page: AlbumSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: AlbumViewerRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: AlbumAdditionalSharedUserSelectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), - AutoRoute(page: BackupAlbumSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumPreviewRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: FailedBackupStatusRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, - ), AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]), AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: ArchiveRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PartnerRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), AutoRoute(page: FolderRoute.page, guards: [_authGuard]), - AutoRoute(page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: PersonResultRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AllPeopleRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MemoryRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: MapRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SharedLinkEditRoute.page, guards: [_authGuard, _duplicateGuard]), - CustomRoute( - page: ActivitiesRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - durationInMilliseconds: 200, - ), CustomRoute(page: MapLocationPickerRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: HeaderSettingsRoute.page, guards: [_duplicateGuard]), - CustomRoute( - page: PeopleCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: AlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: LocalAlbumsRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - CustomRoute( - page: PlacesCollectionRoute.page, - guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideLeft, - ), - AutoRoute(page: NativeVideoViewerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), @@ -323,7 +169,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), @@ -333,8 +178,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftAlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftMapRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftEditImageRoute.page), - AutoRoute(page: DriftCropImageRoute.page), - AutoRoute(page: DriftFilterImageRoute.page), AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 2d57c16573..07c3a52b49 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -10,330 +10,6 @@ part of 'router.dart'; -/// generated route for -/// [ActivitiesPage] -class ActivitiesRoute extends PageRouteInfo { - const ActivitiesRoute({List? children}) - : super(ActivitiesRoute.name, initialChildren: children); - - static const String name = 'ActivitiesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ActivitiesPage(); - }, - ); -} - -/// generated route for -/// [AlbumAdditionalSharedUserSelectionPage] -class AlbumAdditionalSharedUserSelectionRoute - extends PageRouteInfo { - AlbumAdditionalSharedUserSelectionRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumAdditionalSharedUserSelectionRoute.name, - args: AlbumAdditionalSharedUserSelectionRouteArgs( - key: key, - album: album, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAdditionalSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAdditionalSharedUserSelectionPage( - key: args.key, - album: args.album, - ); - }, - ); -} - -class AlbumAdditionalSharedUserSelectionRouteArgs { - const AlbumAdditionalSharedUserSelectionRouteArgs({ - this.key, - required this.album, - }); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumAdditionalSharedUserSelectionRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumAssetSelectionPage] -class AlbumAssetSelectionRoute - extends PageRouteInfo { - AlbumAssetSelectionRoute({ - Key? key, - required Set existingAssets, - bool canDeselect = false, - List? children, - }) : super( - AlbumAssetSelectionRoute.name, - args: AlbumAssetSelectionRouteArgs( - key: key, - existingAssets: existingAssets, - canDeselect: canDeselect, - ), - initialChildren: children, - ); - - static const String name = 'AlbumAssetSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumAssetSelectionPage( - key: args.key, - existingAssets: args.existingAssets, - canDeselect: args.canDeselect, - ); - }, - ); -} - -class AlbumAssetSelectionRouteArgs { - const AlbumAssetSelectionRouteArgs({ - this.key, - required this.existingAssets, - this.canDeselect = false, - }); - - final Key? key; - - final Set existingAssets; - - final bool canDeselect; - - @override - String toString() { - return 'AlbumAssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect}'; - } -} - -/// generated route for -/// [AlbumOptionsPage] -class AlbumOptionsRoute extends PageRouteInfo { - const AlbumOptionsRoute({List? children}) - : super(AlbumOptionsRoute.name, initialChildren: children); - - static const String name = 'AlbumOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumOptionsPage(); - }, - ); -} - -/// generated route for -/// [AlbumPreviewPage] -class AlbumPreviewRoute extends PageRouteInfo { - AlbumPreviewRoute({ - Key? key, - required Album album, - List? children, - }) : super( - AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'AlbumPreviewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumPreviewPage(key: args.key, album: args.album); - }, - ); -} - -class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({this.key, required this.album}); - - final Key? key; - - final Album album; - - @override - String toString() { - return 'AlbumPreviewRouteArgs{key: $key, album: $album}'; - } -} - -/// generated route for -/// [AlbumSharedUserSelectionPage] -class AlbumSharedUserSelectionRoute - extends PageRouteInfo { - AlbumSharedUserSelectionRoute({ - Key? key, - required Set assets, - List? children, - }) : super( - AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'AlbumSharedUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); - }, - ); -} - -class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); - - final Key? key; - - final Set assets; - - @override - String toString() { - return 'AlbumSharedUserSelectionRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [AlbumViewerPage] -class AlbumViewerRoute extends PageRouteInfo { - AlbumViewerRoute({ - Key? key, - required int albumId, - List? children, - }) : super( - AlbumViewerRoute.name, - args: AlbumViewerRouteArgs(key: key, albumId: albumId), - initialChildren: children, - ); - - static const String name = 'AlbumViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AlbumViewerPage(key: args.key, albumId: args.albumId); - }, - ); -} - -class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({this.key, required this.albumId}); - - final Key? key; - - final int albumId; - - @override - String toString() { - return 'AlbumViewerRouteArgs{key: $key, albumId: $albumId}'; - } -} - -/// generated route for -/// [AlbumsPage] -class AlbumsRoute extends PageRouteInfo { - const AlbumsRoute({List? children}) - : super(AlbumsRoute.name, initialChildren: children); - - static const String name = 'AlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AlbumsPage(); - }, - ); -} - -/// generated route for -/// [AllMotionPhotosPage] -class AllMotionPhotosRoute extends PageRouteInfo { - const AllMotionPhotosRoute({List? children}) - : super(AllMotionPhotosRoute.name, initialChildren: children); - - static const String name = 'AllMotionPhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllMotionPhotosPage(); - }, - ); -} - -/// generated route for -/// [AllPeoplePage] -class AllPeopleRoute extends PageRouteInfo { - const AllPeopleRoute({List? children}) - : super(AllPeopleRoute.name, initialChildren: children); - - static const String name = 'AllPeopleRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPeoplePage(); - }, - ); -} - -/// generated route for -/// [AllPlacesPage] -class AllPlacesRoute extends PageRouteInfo { - const AllPlacesRoute({List? children}) - : super(AllPlacesRoute.name, initialChildren: children); - - static const String name = 'AllPlacesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllPlacesPage(); - }, - ); -} - -/// generated route for -/// [AllVideosPage] -class AllVideosRoute extends PageRouteInfo { - const AllVideosRoute({List? children}) - : super(AllVideosRoute.name, initialChildren: children); - - static const String name = 'AllVideosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AllVideosPage(); - }, - ); -} - /// generated route for /// [AppLogDetailPage] class AppLogDetailRoute extends PageRouteInfo { @@ -387,22 +63,6 @@ class AppLogRoute extends PageRouteInfo { ); } -/// generated route for -/// [ArchivePage] -class ArchiveRoute extends PageRouteInfo { - const ArchiveRoute({List? children}) - : super(ArchiveRoute.name, initialChildren: children); - - static const String name = 'ArchiveRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ArchivePage(); - }, - ); -} - /// generated route for /// [AssetTroubleshootPage] class AssetTroubleshootRoute extends PageRouteInfo { @@ -504,97 +164,6 @@ class AssetViewerRouteArgs { } } -/// generated route for -/// [BackupAlbumSelectionPage] -class BackupAlbumSelectionRoute extends PageRouteInfo { - const BackupAlbumSelectionRoute({List? children}) - : super(BackupAlbumSelectionRoute.name, initialChildren: children); - - static const String name = 'BackupAlbumSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupAlbumSelectionPage(); - }, - ); -} - -/// generated route for -/// [BackupControllerPage] -class BackupControllerRoute extends PageRouteInfo { - const BackupControllerRoute({List? children}) - : super(BackupControllerRoute.name, initialChildren: children); - - static const String name = 'BackupControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupControllerPage(); - }, - ); -} - -/// generated route for -/// [BackupOptionsPage] -class BackupOptionsRoute extends PageRouteInfo { - const BackupOptionsRoute({List? children}) - : super(BackupOptionsRoute.name, initialChildren: children); - - static const String name = 'BackupOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const BackupOptionsPage(); - }, - ); -} - -/// generated route for -/// [ChangeExperiencePage] -class ChangeExperienceRoute extends PageRouteInfo { - ChangeExperienceRoute({ - Key? key, - required bool switchingToBeta, - List? children, - }) : super( - ChangeExperienceRoute.name, - args: ChangeExperienceRouteArgs( - key: key, - switchingToBeta: switchingToBeta, - ), - initialChildren: children, - ); - - static const String name = 'ChangeExperienceRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ChangeExperiencePage( - key: args.key, - switchingToBeta: args.switchingToBeta, - ); - }, - ); -} - -class ChangeExperienceRouteArgs { - const ChangeExperienceRouteArgs({this.key, required this.switchingToBeta}); - - final Key? key; - - final bool switchingToBeta; - - @override - String toString() { - return 'ChangeExperienceRouteArgs{key: $key, switchingToBeta: $switchingToBeta}'; - } -} - /// generated route for /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { @@ -648,89 +217,6 @@ class CleanupPreviewRouteArgs { } } -/// generated route for -/// [CreateAlbumPage] -class CreateAlbumRoute extends PageRouteInfo { - CreateAlbumRoute({ - Key? key, - List? assets, - List? children, - }) : super( - CreateAlbumRoute.name, - args: CreateAlbumRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'CreateAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs(), - ); - return CreateAlbumPage(key: args.key, assets: args.assets); - }, - ); -} - -class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({this.key, this.assets}); - - final Key? key; - - final List? assets; - - @override - String toString() { - return 'CreateAlbumRouteArgs{key: $key, assets: $assets}'; - } -} - -/// generated route for -/// [CropImagePage] -class CropImageRoute extends PageRouteInfo { - CropImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - CropImageRoute.name, - args: CropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'CropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return CropImagePage(key: args.key, image: args.image, asset: args.asset); - }, - ); -} - -class CropImageRouteArgs { - const CropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DownloadInfoPage] class DownloadInfoRoute extends PageRouteInfo { @@ -1003,70 +489,20 @@ class DriftCreateAlbumRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftCropImagePage] -class DriftCropImageRoute extends PageRouteInfo { - DriftCropImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftCropImageRoute.name, - args: DriftCropImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftCropImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftCropImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftCropImageRouteArgs { - const DriftCropImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftCropImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftEditImagePage] class DriftEditImageRoute extends PageRouteInfo { DriftEditImageRoute({ Key? key, - required BaseAsset asset, required Image image, - required bool isEdited, + required Future Function(List) applyEdits, List? children, }) : super( DriftEditImageRoute.name, args: DriftEditImageRouteArgs( key: key, - asset: asset, image: image, - isEdited: isEdited, + applyEdits: applyEdits, ), initialChildren: children, ); @@ -1079,9 +515,8 @@ class DriftEditImageRoute extends PageRouteInfo { final args = data.argsAs(); return DriftEditImagePage( key: args.key, - asset: args.asset, image: args.image, - isEdited: args.isEdited, + applyEdits: args.applyEdits, ); }, ); @@ -1090,22 +525,19 @@ class DriftEditImageRoute extends PageRouteInfo { class DriftEditImageRouteArgs { const DriftEditImageRouteArgs({ this.key, - required this.asset, required this.image, - required this.isEdited, + required this.applyEdits, }); final Key? key; - final BaseAsset asset; - final Image image; - final bool isEdited; + final Future Function(List) applyEdits; @override String toString() { - return 'DriftEditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; + return 'DriftEditImageRouteArgs{key: $key, image: $image, applyEdits: $applyEdits}'; } } @@ -1125,54 +557,6 @@ class DriftFavoriteRoute extends PageRouteInfo { ); } -/// generated route for -/// [DriftFilterImagePage] -class DriftFilterImageRoute extends PageRouteInfo { - DriftFilterImageRoute({ - Key? key, - required Image image, - required BaseAsset asset, - List? children, - }) : super( - DriftFilterImageRoute.name, - args: DriftFilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'DriftFilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftFilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class DriftFilterImageRouteArgs { - const DriftFilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final BaseAsset asset; - - @override - String toString() { - return 'DriftFilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [DriftLibraryPage] class DriftLibraryRoute extends PageRouteInfo { @@ -1616,144 +1000,6 @@ class DriftVideoRoute extends PageRouteInfo { ); } -/// generated route for -/// [EditImagePage] -class EditImageRoute extends PageRouteInfo { - EditImageRoute({ - Key? key, - required Asset asset, - required Image image, - required bool isEdited, - List? children, - }) : super( - EditImageRoute.name, - args: EditImageRouteArgs( - key: key, - asset: asset, - image: image, - isEdited: isEdited, - ), - initialChildren: children, - ); - - static const String name = 'EditImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return EditImagePage( - key: args.key, - asset: args.asset, - image: args.image, - isEdited: args.isEdited, - ); - }, - ); -} - -class EditImageRouteArgs { - const EditImageRouteArgs({ - this.key, - required this.asset, - required this.image, - required this.isEdited, - }); - - final Key? key; - - final Asset asset; - - final Image image; - - final bool isEdited; - - @override - String toString() { - return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; - } -} - -/// generated route for -/// [FailedBackupStatusPage] -class FailedBackupStatusRoute extends PageRouteInfo { - const FailedBackupStatusRoute({List? children}) - : super(FailedBackupStatusRoute.name, initialChildren: children); - - static const String name = 'FailedBackupStatusRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FailedBackupStatusPage(); - }, - ); -} - -/// generated route for -/// [FavoritesPage] -class FavoritesRoute extends PageRouteInfo { - const FavoritesRoute({List? children}) - : super(FavoritesRoute.name, initialChildren: children); - - static const String name = 'FavoritesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const FavoritesPage(); - }, - ); -} - -/// generated route for -/// [FilterImagePage] -class FilterImageRoute extends PageRouteInfo { - FilterImageRoute({ - Key? key, - required Image image, - required Asset asset, - List? children, - }) : super( - FilterImageRoute.name, - args: FilterImageRouteArgs(key: key, image: image, asset: asset), - initialChildren: children, - ); - - static const String name = 'FilterImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return FilterImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); - }, - ); -} - -class FilterImageRouteArgs { - const FilterImageRouteArgs({ - this.key, - required this.image, - required this.asset, - }); - - final Key? key; - - final Image image; - - final Asset asset; - - @override - String toString() { - return 'FilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; - } -} - /// generated route for /// [FolderPage] class FolderRoute extends PageRouteInfo { @@ -1793,70 +1039,6 @@ class FolderRouteArgs { } } -/// generated route for -/// [GalleryViewerPage] -class GalleryViewerRoute extends PageRouteInfo { - GalleryViewerRoute({ - Key? key, - required RenderList renderList, - int initialIndex = 0, - int heroOffset = 0, - bool showStack = false, - List? children, - }) : super( - GalleryViewerRoute.name, - args: GalleryViewerRouteArgs( - key: key, - renderList: renderList, - initialIndex: initialIndex, - heroOffset: heroOffset, - showStack: showStack, - ), - initialChildren: children, - ); - - static const String name = 'GalleryViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return GalleryViewerPage( - key: args.key, - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ); - }, - ); -} - -class GalleryViewerRouteArgs { - const GalleryViewerRouteArgs({ - this.key, - required this.renderList, - this.initialIndex = 0, - this.heroOffset = 0, - this.showStack = false, - }); - - final Key? key; - - final RenderList renderList; - - final int initialIndex; - - final int heroOffset; - - final bool showStack; - - @override - String toString() { - return 'GalleryViewerRouteArgs{key: $key, renderList: $renderList, initialIndex: $initialIndex, heroOffset: $heroOffset, showStack: $showStack}'; - } -} - /// generated route for /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo { @@ -1873,38 +1055,6 @@ class HeaderSettingsRoute extends PageRouteInfo { ); } -/// generated route for -/// [LibraryPage] -class LibraryRoute extends PageRouteInfo { - const LibraryRoute({List? children}) - : super(LibraryRoute.name, initialChildren: children); - - static const String name = 'LibraryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LibraryPage(); - }, - ); -} - -/// generated route for -/// [LocalAlbumsPage] -class LocalAlbumsRoute extends PageRouteInfo { - const LocalAlbumsRoute({List? children}) - : super(LocalAlbumsRoute.name, initialChildren: children); - - static const String name = 'LocalAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LocalAlbumsPage(); - }, - ); -} - /// generated route for /// [LocalMediaSummaryPage] class LocalMediaSummaryRoute extends PageRouteInfo { @@ -1958,22 +1108,6 @@ class LocalTimelineRouteArgs { } } -/// generated route for -/// [LockedPage] -class LockedRoute extends PageRouteInfo { - const LockedRoute({List? children}) - : super(LockedRoute.name, initialChildren: children); - - static const String name = 'LockedRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LockedPage(); - }, - ); -} - /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { @@ -2054,311 +1188,6 @@ class MapLocationPickerRouteArgs { } } -/// generated route for -/// [MapPage] -class MapRoute extends PageRouteInfo { - MapRoute({Key? key, LatLng? initialLocation, List? children}) - : super( - MapRoute.name, - args: MapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); - - static const String name = 'MapRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const MapRouteArgs(), - ); - return MapPage(key: args.key, initialLocation: args.initialLocation); - }, - ); -} - -class MapRouteArgs { - const MapRouteArgs({this.key, this.initialLocation}); - - final Key? key; - - final LatLng? initialLocation; - - @override - String toString() { - return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}'; - } -} - -/// generated route for -/// [MemoryPage] -class MemoryRoute extends PageRouteInfo { - MemoryRoute({ - required List memories, - required int memoryIndex, - Key? key, - List? children, - }) : super( - MemoryRoute.name, - args: MemoryRouteArgs( - memories: memories, - memoryIndex: memoryIndex, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'MemoryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return MemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ); - }, - ); -} - -class MemoryRouteArgs { - const MemoryRouteArgs({ - required this.memories, - required this.memoryIndex, - this.key, - }); - - final List memories; - - final int memoryIndex; - - final Key? key; - - @override - String toString() { - return 'MemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; - } -} - -/// generated route for -/// [NativeVideoViewerPage] -class NativeVideoViewerRoute extends PageRouteInfo { - NativeVideoViewerRoute({ - Key? key, - required Asset asset, - required Widget image, - bool showControls = true, - int playbackDelayFactor = 1, - List? children, - }) : super( - NativeVideoViewerRoute.name, - args: NativeVideoViewerRouteArgs( - key: key, - asset: asset, - image: image, - showControls: showControls, - playbackDelayFactor: playbackDelayFactor, - ), - initialChildren: children, - ); - - static const String name = 'NativeVideoViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return NativeVideoViewerPage( - key: args.key, - asset: args.asset, - image: args.image, - showControls: args.showControls, - playbackDelayFactor: args.playbackDelayFactor, - ); - }, - ); -} - -class NativeVideoViewerRouteArgs { - const NativeVideoViewerRouteArgs({ - this.key, - required this.asset, - required this.image, - this.showControls = true, - this.playbackDelayFactor = 1, - }); - - final Key? key; - - final Asset asset; - - final Widget image; - - final bool showControls; - - final int playbackDelayFactor; - - @override - String toString() { - return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, image: $image, showControls: $showControls, playbackDelayFactor: $playbackDelayFactor}'; - } -} - -/// generated route for -/// [PartnerDetailPage] -class PartnerDetailRoute extends PageRouteInfo { - PartnerDetailRoute({ - Key? key, - required UserDto partner, - List? children, - }) : super( - PartnerDetailRoute.name, - args: PartnerDetailRouteArgs(key: key, partner: partner), - initialChildren: children, - ); - - static const String name = 'PartnerDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PartnerDetailPage(key: args.key, partner: args.partner); - }, - ); -} - -class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({this.key, required this.partner}); - - final Key? key; - - final UserDto partner; - - @override - String toString() { - return 'PartnerDetailRouteArgs{key: $key, partner: $partner}'; - } -} - -/// generated route for -/// [PartnerPage] -class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); - - static const String name = 'PartnerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PartnerPage(); - }, - ); -} - -/// generated route for -/// [PeopleCollectionPage] -class PeopleCollectionRoute extends PageRouteInfo { - const PeopleCollectionRoute({List? children}) - : super(PeopleCollectionRoute.name, initialChildren: children); - - static const String name = 'PeopleCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PeopleCollectionPage(); - }, - ); -} - -/// generated route for -/// [PermissionOnboardingPage] -class PermissionOnboardingRoute extends PageRouteInfo { - const PermissionOnboardingRoute({List? children}) - : super(PermissionOnboardingRoute.name, initialChildren: children); - - static const String name = 'PermissionOnboardingRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PermissionOnboardingPage(); - }, - ); -} - -/// generated route for -/// [PersonResultPage] -class PersonResultRoute extends PageRouteInfo { - PersonResultRoute({ - Key? key, - required String personId, - required String personName, - List? children, - }) : super( - PersonResultRoute.name, - args: PersonResultRouteArgs( - key: key, - personId: personId, - personName: personName, - ), - initialChildren: children, - ); - - static const String name = 'PersonResultRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return PersonResultPage( - key: args.key, - personId: args.personId, - personName: args.personName, - ); - }, - ); -} - -class PersonResultRouteArgs { - const PersonResultRouteArgs({ - this.key, - required this.personId, - required this.personName, - }); - - final Key? key; - - final String personId; - - final String personName; - - @override - String toString() { - return 'PersonResultRouteArgs{key: $key, personId: $personId, personName: $personName}'; - } -} - -/// generated route for -/// [PhotosPage] -class PhotosRoute extends PageRouteInfo { - const PhotosRoute({List? children}) - : super(PhotosRoute.name, initialChildren: children); - - static const String name = 'PhotosRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PhotosPage(); - }, - ); -} - /// generated route for /// [PinAuthPage] class PinAuthRoute extends PageRouteInfo { @@ -2398,51 +1227,6 @@ class PinAuthRouteArgs { } } -/// generated route for -/// [PlacesCollectionPage] -class PlacesCollectionRoute extends PageRouteInfo { - PlacesCollectionRoute({ - Key? key, - LatLng? currentLocation, - List? children, - }) : super( - PlacesCollectionRoute.name, - args: PlacesCollectionRouteArgs( - key: key, - currentLocation: currentLocation, - ), - initialChildren: children, - ); - - static const String name = 'PlacesCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs(), - ); - return PlacesCollectionPage( - key: args.key, - currentLocation: args.currentLocation, - ); - }, - ); -} - -class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({this.key, this.currentLocation}); - - final Key? key; - - final LatLng? currentLocation; - - @override - String toString() { - return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}'; - } -} - /// generated route for /// [ProfilePictureCropPage] class ProfilePictureCropRoute @@ -2481,22 +1265,6 @@ class ProfilePictureCropRouteArgs { } } -/// generated route for -/// [RecentlyTakenPage] -class RecentlyTakenRoute extends PageRouteInfo { - const RecentlyTakenRoute({List? children}) - : super(RecentlyTakenRoute.name, initialChildren: children); - - static const String name = 'RecentlyTakenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const RecentlyTakenPage(); - }, - ); -} - /// generated route for /// [RemoteAlbumPage] class RemoteAlbumRoute extends PageRouteInfo { @@ -2550,45 +1318,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { ); } -/// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ - Key? key, - SearchFilter? prefilter, - List? children, - }) : super( - SearchRoute.name, - args: SearchRouteArgs(key: key, prefilter: prefilter), - initialChildren: children, - ); - - static const String name = 'SearchRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const SearchRouteArgs(), - ); - return SearchPage(key: args.key, prefilter: args.prefilter); - }, - ); -} - -class SearchRouteArgs { - const SearchRouteArgs({this.key, this.prefilter}); - - final Key? key; - - final SearchFilter? prefilter; - - @override - String toString() { - return 'SearchRouteArgs{key: $key, prefilter: $prefilter}'; - } -} - /// generated route for /// [SettingsPage] class SettingsRoute extends PageRouteInfo { @@ -2787,22 +1516,6 @@ class SyncStatusRoute extends PageRouteInfo { ); } -/// generated route for -/// [TabControllerPage] -class TabControllerRoute extends PageRouteInfo { - const TabControllerRoute({List? children}) - : super(TabControllerRoute.name, initialChildren: children); - - static const String name = 'TabControllerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TabControllerPage(); - }, - ); -} - /// generated route for /// [TabShellPage] class TabShellRoute extends PageRouteInfo { @@ -2818,19 +1531,3 @@ class TabShellRoute extends PageRouteInfo { }, ); } - -/// generated route for -/// [TrashPage] -class TrashRoute extends PageRouteInfo { - const TrashRoute({List? children}) - : super(TrashRoute.name, initialChildren: children); - - static const String name = 'TrashRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TrashPage(); - }, - ); -} diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index c435bf9d79..1a6333215a 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; @@ -246,6 +247,14 @@ class ActionService { return true; } + Future applyEdits(String remoteId, List edits) async { + if (edits.isEmpty) { + await _assetApiRepository.removeEdits(remoteId); + } else { + await _assetApiRepository.editAsset(remoteId, edits); + } + } + Future _deleteLocalAssets(List localIds) async { final deletedIds = await _assetMediaRepository.deleteAll(localIds); if (deletedIds.isEmpty) { diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 382a7fe107..0ef1badacb 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -9,7 +9,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:logging/logging.dart'; -import 'package:immich_mobile/entities/store.entity.dart' as immich_store; class ActivityService with ErrorLoggerMixin { final ActivityApiRepository _activityApiRepository; @@ -60,20 +59,16 @@ class ActivityService with ErrorLoggerMixin { } Future buildAssetViewerRoute(String assetId, WidgetRef ref) async { - if (immich_store.Store.isBetaTimelineEnabled) { - final asset = await _assetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), - currentAlbum: ref.read(currentRemoteAlbumProvider), - ); + final asset = await _assetService.getRemoteAsset(assetId); + if (asset == null) { + return null; } - return null; + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.albumActivities), + currentAlbum: ref.read(currentRemoteAlbumProvider), + ); } } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart deleted file mode 100644 index 8d77b569e6..0000000000 --- a/mobile/lib/services/album.service.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; -import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; -import 'package:immich_mobile/models/albums/album_search.model.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final albumServiceProvider = Provider( - (ref) => AlbumService( - ref.watch(syncServiceProvider), - ref.watch(userServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ), -); - -class AlbumService { - final SyncService _syncService; - final UserService _userService; - final EntityService _entityService; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final BackupAlbumRepository _backupAlbumRepository; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final Logger _log = Logger('AlbumService'); - Completer _localCompleter = Completer()..complete(false); - Completer _remoteCompleter = Completer()..complete(false); - - AlbumService( - this._syncService, - this._userService, - this._entityService, - this._albumRepository, - this._assetRepository, - this._backupAlbumRepository, - this._albumMediaRepository, - this._albumApiRepository, - ); - - /// Checks all selected device albums for changes of albums and their assets - /// Updates the local database and returns `true` if there were any changes - Future refreshDeviceAlbums() async { - if (!_localCompleter.isCompleted) { - // guard against concurrent calls - _log.info("refreshDeviceAlbums is already in progress"); - return _localCompleter.future; - } - _localCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final (selectedIds, excludedIds, onDevice) = await ( - _backupAlbumRepository.getIdsBySelection(BackupSelection.select).then((value) => value.toSet()), - _backupAlbumRepository.getIdsBySelection(BackupSelection.exclude).then((value) => value.toSet()), - _albumMediaRepository.getAll(), - ).wait; - _log.info("Found ${onDevice.length} device albums"); - if (selectedIds.isEmpty) { - final numLocal = await _albumRepository.count(local: true); - if (numLocal > 0) { - await _syncService.removeAllLocalAlbumsAndAssets(); - } - return false; - } - Set? excludedAssets; - if (excludedIds.isNotEmpty) { - if (Platform.isIOS) { - // iOS and Android device album working principle differ significantly - // on iOS, an asset can be in multiple albums - // on Android, an asset can only be in exactly one album (folder!) at the same time - // thus, on Android, excluding an album can be done by ignoring that album - // however, on iOS, it it necessary to load the assets from all excluded - // albums and check every asset from any selected album against the set - // of excluded assets - excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds); - _log.info("Found ${excludedAssets.length} assets to exclude"); - } - // remove all excluded albums - onDevice.removeWhere((e) => excludedIds.contains(e.localId)); - _log.info("Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums"); - } - - final allAlbum = onDevice.firstWhereOrNull((album) => album.isAll); - final hasAll = allAlbum != null && selectedIds.contains(allAlbum.localId); - if (hasAll) { - if (Platform.isAndroid) { - // remove the virtual "Recent" album and keep and individual albums - // on Android, the virtual "Recent" `lastModified` value is always null - onDevice.removeWhere((album) => album.isAll); - _log.info("'Recents' is selected, keeping all individual albums"); - } - } else { - // keep only the explicitly selected albums - onDevice.removeWhere((album) => !selectedIds.contains(album.localId)); - _log.info("'Recents' is not selected, keeping only selected albums"); - } - changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); - _log.info("Syncing completed. Changes: $changes"); - } finally { - _localCompleter.complete(changes); - } - dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future> _loadExcludedAssetIds(List albums, Set excludedAlbumIds) async { - final Set result = HashSet(); - for (final batchAlbums in albums.where((album) => excludedAlbumIds.contains(album.localId)).slices(5)) { - await batchAlbums - .map((album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds))) - .wait; - } - return result; - } - - /// Checks remote albums (owned if `isShared` is false) for changes, - /// updates the local database and returns `true` if there were any changes - Future refreshRemoteAlbums() async { - if (!_remoteCompleter.isCompleted) { - // guard against concurrent calls - return _remoteCompleter.future; - } - _remoteCompleter = Completer(); - final Stopwatch sw = Stopwatch()..start(); - bool changes = false; - try { - final users = await _syncService.getUsersFromServer(); - if (users != null) { - await _syncService.syncUsersFromServer(users); - } - final (sharedAlbum, ownedAlbum) = await ( - // Note: `shared: true` is required to get albums that don't belong to - // us due to unusual behaviour on the API but this will also return our - // own shared albums - _albumApiRepository.getAll(shared: true), - // Passing null (or nothing) for `shared` returns only albums that - // explicitly belong to us - _albumApiRepository.getAll(shared: null), - ).wait; - - final albums = HashSet(equals: (a, b) => a.remoteId == b.remoteId, hashCode: (a) => a.remoteId.hashCode); - - albums.addAll(sharedAlbum); - albums.addAll(ownedAlbum); - - changes = await _syncService.syncRemoteAlbumsToDb(albums.toList()); - } finally { - _remoteCompleter.complete(changes); - } - dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - Future createAlbum( - String albumName, - Iterable assets, [ - Iterable sharedUsers = const [], - ]) async { - final Album album = await _albumApiRepository.create( - albumName, - assetIds: assets.map((asset) => asset.remoteId!), - sharedUserIds: sharedUsers.map((user) => user.id), - ); - await _entityService.fillAlbumWithDatabaseEntities(album); - return _albumRepository.create(album); - } - - /* - * Creates names like Untitled, Untitled (1), Untitled (2), ... - */ - Future _getNextAlbumName() async { - const baseName = "Untitled"; - for (int round = 0; ; round++) { - final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - - if (null == await _albumRepository.getByName(proposedName, owner: true)) { - return proposedName; - } - } - } - - Future createAlbumWithGeneratedName(Iterable assets) async { - return createAlbum(await _getNextAlbumName(), assets, []); - } - - Future addAssets(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.addAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - - final List addedAssets = result.added - .map((id) => assets.firstWhere((asset) => asset.remoteId == id)) - .toList(); - - await _updateAssets(album.id, add: addedAssets); - - return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); - } catch (e) { - dPrint(() => "Error addAssets ${e.toString()}"); - } - return null; - } - - Future _updateAssets(int albumId, {List add = const [], List remove = const []}) => - _albumRepository.transaction(() async { - final album = await _albumRepository.get(albumId); - if (album == null) return; - await _albumRepository.addAssets(album, add); - await _albumRepository.removeAssets(album, remove); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - - Future setActivityStatus(Album album, bool enabled) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, activityEnabled: enabled); - album.activityEnabled = updatedAlbum.activityEnabled; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error setActivityEnabled ${e.toString()}"); - } - return false; - } - - Future deleteAlbum(Album album) async { - try { - final userId = _userService.getMyUser().id; - if (album.owner.value?.isarId == fastHash(userId)) { - await _albumApiRepository.delete(album.remoteId!); - } - if (album.shared) { - final foreignAssets = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - await _albumRepository.delete(album.id); - - final List albums = await _albumRepository.getAll(shared: true); - final List existing = []; - for (Album album in albums) { - existing.addAll(await _assetRepository.getByAlbum(album, notOwnedBy: [userId])); - } - final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - await _albumRepository.delete(album.id); - } - return true; - } catch (e) { - dPrint(() => "Error deleteAlbum ${e.toString()}"); - } - return false; - } - - Future leaveAlbum(Album album) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); - return true; - } catch (e) { - dPrint(() => "Error leaveAlbum ${e.toString()}"); - return false; - } - } - - Future removeAsset(Album album, Iterable assets) async { - try { - final result = await _albumApiRepository.removeAssets(album.remoteId!, assets.map((asset) => asset.remoteId!)); - final toRemove = result.removed.map((id) => assets.firstWhere((asset) => asset.remoteId == id)); - await _updateAssets(album.id, remove: toRemove.toList()); - return true; - } catch (e) { - dPrint(() => "Error removeAssetFromAlbum ${e.toString()}"); - } - return false; - } - - Future removeUser(Album album, UserDto user) async { - try { - await _albumApiRepository.removeUser(album.remoteId!, userId: user.id); - - album.sharedUsers.remove(entity.User.fromDto(user)); - await _albumRepository.removeUsers(album, [user]); - final a = await _albumRepository.get(album.id); - // trigger watcher - await _albumRepository.update(a!); - - return true; - } catch (error) { - dPrint(() => "Error removeUser ${error.toString()}"); - return false; - } - } - - Future addUsers(Album album, List userIds) async { - try { - final updatedAlbum = await _albumApiRepository.addUsers(album.remoteId!, userIds); - - album.sharedUsers.addAll(updatedAlbum.remoteUsers); - album.shared = true; - - await _albumRepository.addUsers(album, album.sharedUsers.map((u) => u.toDto()).toList()); - await _albumRepository.update(album); - - return true; - } catch (error) { - dPrint(() => "Error addUsers ${error.toString()}"); - } - return false; - } - - Future changeTitleAlbum(Album album, String newAlbumTitle) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, name: newAlbumTitle); - - album.name = updatedAlbum.name; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeTitleAlbum ${e.toString()}"); - return false; - } - } - - Future changeDescriptionAlbum(Album album, String newAlbumDescription) async { - try { - final updatedAlbum = await _albumApiRepository.update(album.remoteId!, description: newAlbumDescription); - - album.description = updatedAlbum.description; - await _albumRepository.update(album); - return true; - } catch (e) { - dPrint(() => "Error changeDescriptionAlbum ${e.toString()}"); - return false; - } - } - - Future getAlbumByName(String name, {bool? remote, bool? shared, bool? owner}) => - _albumRepository.getByName(name, remote: remote, shared: shared, owner: owner); - - /// - /// Add the uploaded asset to the selected albums - /// - Future syncUploadAlbums(List albumNames, List assetIds) async { - for (final albumName in albumNames) { - Album? album = await getAlbumByName(albumName, remote: true, owner: true); - album ??= await createAlbum(albumName, []); - if (album != null && album.remoteId != null) { - await _albumApiRepository.addAssets(album.remoteId!, assetIds); - } - } - } - - Future> getAllRemoteAlbums() async { - return _albumRepository.getAll(remote: true); - } - - Future> getAllLocalAlbums() async { - return _albumRepository.getAll(remote: false); - } - - Stream> watchRemoteAlbums() { - return _albumRepository.watchRemoteAlbums(); - } - - Stream> watchLocalAlbums() { - return _albumRepository.watchLocalAlbums(); - } - - /// Get album by Isar ID - Future getAlbumById(int id) { - return _albumRepository.get(id); - } - - Future getAlbumByRemoteId(String remoteId) { - return _albumRepository.getByRemoteId(remoteId); - } - - Stream watchAlbum(int id) { - return _albumRepository.watchAlbum(id); - } - - Future> search(String searchTerm, QuickFilterMode filterMode) async { - return _albumRepository.search(searchTerm, filterMode); - } - - Future updateSortOrder(Album album, SortOrder order) async { - try { - final updateAlbum = await _albumApiRepository.update(album.remoteId!, sortOrder: order); - album.sortOrder = updateAlbum.sortOrder; - - return _albumRepository.update(album); - } catch (error, stackTrace) { - _log.severe("Error updating album sort order", error, stackTrace); - } - return null; - } - - Future clearTable() async { - await _albumRepository.clearTable(); - } -} diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart deleted file mode 100644 index b9fab35442..0000000000 --- a/mobile/lib/services/asset.service.dart +++ /dev/null @@ -1,465 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final assetServiceProvider = Provider( - (ref) => AssetService( - ref.watch(assetApiRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(etagRepositoryProvider), - ref.watch(backupAlbumRepositoryProvider), - ref.watch(apiServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(backupServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class AssetService { - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final ETagRepository _etagRepository; - final BackupAlbumRepository _backupRepository; - final ApiService _apiService; - final SyncService _syncService; - final BackupService _backupService; - final AlbumService _albumService; - final UserService _userService; - final AssetMediaRepository _assetMediaRepository; - final log = Logger('AssetService'); - - AssetService( - this._assetApiRepository, - this._assetRepository, - this._exifInfoRepository, - this._isarUserRepository, - this._etagRepository, - this._backupRepository, - this._apiService, - this._syncService, - this._backupService, - this._albumService, - this._userService, - this._assetMediaRepository, - ); - - /// Checks the server for updated assets and updates the local database if - /// required. Returns `true` if there were any changes. - Future refreshRemoteAssets() async { - final syncedUserIds = await _etagRepository.getAllIds(); - final List syncedUsers = syncedUserIds.isEmpty - ? [] - : (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList(); - final Stopwatch sw = Stopwatch()..start(); - final bool changes = await _syncService.syncRemoteAssetsToDb( - users: syncedUsers, - getChangedAssets: _getRemoteAssetChanges, - loadAssets: _getRemoteAssets, - ); - dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); - return changes; - } - - /// Returns `(null, null)` if changes are invalid -> requires full sync - Future<(List? toUpsert, List? toDelete)> _getRemoteAssetChanges( - List users, - DateTime since, - ) async { - final dto = AssetDeltaSyncDto(updatedAfter: since, userIds: users.map((e) => e.id).toList()); - final changes = await _apiService.syncApi.getDeltaSync(dto); - return changes == null || changes.needsFullSync - ? (null, null) - : (changes.upserted.map(Asset.remote).toList(), changes.deleted); - } - - /// Returns the list of people of the given asset id. - // If the server is not reachable `null` is returned. - Future?> getRemotePeopleOfAsset(String remoteId) async { - try { - final AssetResponseDto? dto = await _apiService.assetsApi.getAssetInfo(remoteId); - - return dto?.people; - } catch (error, stack) { - log.severe('Error while getting remote asset info: ${error.toString()}', error, stack); - - return null; - } - } - - /// Returns `null` if the server state did not change, else list of assets - Future?> _getRemoteAssets(UserDto user, DateTime until) async { - const int chunkSize = 10000; - try { - final List allAssets = []; - String? lastId; - // will break on error or once all assets are loaded - while (true) { - final dto = AssetFullSyncDto(limit: chunkSize, updatedUntil: until, lastId: lastId, userId: user.id); - log.fine("Requesting $chunkSize assets from $lastId"); - final List? assets = await _apiService.syncApi.getFullSyncForUser(dto); - if (assets == null) return null; - log.fine("Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}"); - allAssets.addAll(assets.map(Asset.remote)); - if (assets.length != chunkSize) break; - lastId = assets.last.id; - } - return allAssets; - } catch (error, stack) { - log.severe('Error while getting remote assets', error, stack); - return null; - } - } - - /// Loads the exif information from the database. If there is none, loads - /// the exif info from the server (remote assets only) - Future loadExif(Asset a) async { - a.exifInfo ??= (await _exifInfoRepository.get(a.id)); - // fileSize is always filled on the server but not set on client - if (a.exifInfo?.fileSize == null) { - if (a.isRemote) { - final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!); - if (dto != null && dto.exifInfo != null) { - final newExif = Asset.remote(dto).exifInfo!.copyWith(assetId: a.id); - a.exifInfo = newExif; - if (newExif != a.exifInfo) { - if (a.isInDb) { - await _assetRepository.transaction(() => _assetRepository.update(a)); - } else { - dPrint(() => "[loadExif] parameter Asset is not from DB!"); - } - } - } - } else { - // TODO implement local exif info parsing - } - } - return a; - } - - Future updateAssets(List assets, UpdateAssetDto updateAssetDto) async { - return await _apiService.assetsApi.updateAssets( - AssetBulkUpdateDto( - ids: assets.map((e) => e.remoteId!).toList(), - dateTimeOriginal: updateAssetDto.dateTimeOriginal, - isFavorite: updateAssetDto.isFavorite, - visibility: updateAssetDto.visibility, - latitude: updateAssetDto.latitude, - longitude: updateAssetDto.longitude, - ), - ); - } - - Future> changeFavoriteStatus(List assets, bool isFavorite) async { - try { - await updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite)); - - for (var element in assets) { - element.isFavorite = isFavorite; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing favorite status", error, stack); - return []; - } - } - - Future> changeArchiveStatus(List assets, bool isArchived) async { - try { - await updateAssets( - assets, - UpdateAssetDto(visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline), - ); - - for (var element in assets) { - element.isArchived = isArchived; - element.visibility = isArchived ? AssetVisibilityEnum.archive : AssetVisibilityEnum.timeline; - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing archive status", error, stack); - return []; - } - } - - Future?> changeDateTime(List assets, String updatedDt) async { - try { - await updateAssets(assets, UpdateAssetDto(dateTimeOriginal: updatedDt)); - - for (var element in assets) { - element.fileCreatedAt = DateTime.parse(updatedDt); - element.exifInfo = element.exifInfo?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing date/time status", error, stack); - return Future.value(null); - } - } - - Future?> changeLocation(List assets, LatLng location) async { - try { - await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude)); - - for (var element in assets) { - element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude); - } - - await _syncService.upsertAssetsWithExif(assets); - - return assets; - } catch (error, stack) { - log.severe("Error while changing location status", error, stack); - return Future.value(null); - } - } - - Future syncUploadedAssetToAlbums() async { - try { - final selectedAlbums = await _backupRepository.getAllBySelection(BackupSelection.select); - final excludedAlbums = await _backupRepository.getAllBySelection(BackupSelection.exclude); - - final candidates = await _backupService.buildUploadCandidates( - selectedAlbums, - excludedAlbums, - useTimeFilter: false, - ); - - await refreshRemoteAssets(); - final owner = _userService.getMyUser(); - final remoteAssets = await _assetRepository.getAll(ownerId: owner.id, state: AssetState.merged); - - /// Map - Map> assetToAlbums = {}; - - for (BackupCandidate candidate in candidates) { - final asset = remoteAssets.firstWhereOrNull((a) => a.localId == candidate.asset.localId); - - if (asset != null) { - for (final albumName in candidate.albumNames) { - assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); - } - } - } - - // Upload assets to albums - for (final entry in assetToAlbums.entries) { - final albumName = entry.key; - final assetIds = entry.value; - - await _albumService.syncUploadAlbums([albumName], assetIds); - } - } catch (error, stack) { - log.severe("Error while syncing uploaded asset to albums", error, stack); - } - } - - Future setDescription(Asset asset, String newDescription) async { - final remoteAssetId = asset.remoteId; - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (remoteAssetId == null || localExifId == null) { - return; - } - - final result = await _assetApiRepository.update(remoteAssetId, description: newDescription); - - final description = result.exifInfo?.description; - - if (description != null) { - var exifInfo = await _exifInfoRepository.get(localExifId); - - if (exifInfo != null) { - await _exifInfoRepository.update(exifInfo.copyWith(description: description)); - } - } - } - - Future getDescription(Asset asset) async { - final localExifId = asset.exifInfo?.assetId; - - // Guard [remoteAssetId] and [localExifId] null - if (localExifId == null) { - return ""; - } - - final exifInfo = await _exifInfoRepository.get(localExifId); - - return exifInfo?.description ?? ""; - } - - Future getAspectRatio(Asset asset) async { - if (asset.isRemote) { - asset = await loadExif(asset); - } else if (asset.isLocal) { - await asset.localAsync; - } - - final aspectRatio = asset.aspectRatio; - if (aspectRatio != null) { - return aspectRatio; - } - - final width = asset.width; - final height = asset.height; - if (width != null && height != null) { - // we don't know the orientation, so assume it's normal - return width / height; - } - - return 1.0; - } - - Future> getStackAssets(String stackId) { - return _assetRepository.getStackAssets(stackId); - } - - Future clearTable() { - return _assetRepository.clearTable(); - } - - /// Delete assets from local file system and unreference from the database - Future deleteLocalAssets(Iterable assets) async { - // Delete files from local gallery - final candidates = assets.where((asset) => asset.isLocal); - - final deletedIds = await _assetMediaRepository.deleteAll(candidates.map((asset) => asset.localId!).toList()); - - // Modify local database by removing the reference to the local assets - if (deletedIds.isNotEmpty) { - // Delete records from local database - final isarIds = assets.where((asset) => asset.storage == AssetState.local).map((asset) => asset.id).toList(); - await _assetRepository.deleteByIds(isarIds); - - // Modify Merged asset to be remote only - final updatedAssets = assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.localId = null; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - } - - /// Delete assets from the server and unreference from the database - Future deleteRemoteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final candidates = assets.where((a) => a.isRemote); - - if (candidates.isEmpty) { - return; - } - - await _apiService.assetsApi.deleteAssets( - AssetBulkDeleteDto(ids: candidates.map((a) => a.remoteId!).toList(), force: shouldDeletePermanently), - ); - - /// Update asset info bassed on the deletion type. - final payload = shouldDeletePermanently - ? assets.where((asset) => asset.storage == AssetState.merged).map((asset) { - asset.remoteId = null; - asset.visibility = AssetVisibilityEnum.timeline; - return asset; - }) - : assets.where((asset) => asset.isRemote).map((asset) { - asset.isTrashed = true; - return asset; - }); - - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(payload.toList()); - - if (shouldDeletePermanently) { - final remoteAssetIds = assets - .where((asset) => asset.storage == AssetState.remote) - .map((asset) => asset.id) - .toList(); - await _assetRepository.deleteByIds(remoteAssetIds); - } - }); - } - - /// Delete assets on both local file system and the server. - /// Unreference from the database. - Future deleteAssets(Iterable assets, {bool shouldDeletePermanently = false}) async { - final hasLocal = assets.any((asset) => asset.isLocal); - final hasRemote = assets.any((asset) => asset.isRemote); - - if (hasLocal) { - await deleteLocalAssets(assets); - } - - if (hasRemote) { - await deleteRemoteAssets(assets, shouldDeletePermanently: shouldDeletePermanently); - } - } - - Stream watchAsset(int id, {bool fireImmediately = false}) { - return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); - } - - Future> getRecentlyTakenAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getRecentlyTakenAssets(me.id); - } - - Future> getMotionAssets() { - final me = _userService.getMyUser(); - return _assetRepository.getMotionAssets(me.id); - } - - Future setVisibility(List assets, AssetVisibilityEnum visibility) async { - await _assetApiRepository.updateVisibility(assets.map((asset) => asset.remoteId!).toList(), visibility); - - final updatedAssets = assets.map((asset) { - asset.visibility = visibility; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future getAssetByRemoteId(String remoteId) async { - final assets = await _assetRepository.getAllByRemoteId([remoteId]); - return assets.isNotEmpty ? assets.first : null; - } -} diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart deleted file mode 100644 index 03278d25fc..0000000000 --- a/mobile/lib/services/background.service.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/auth.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/localization.service.dart'; -import 'package:immich_mobile/utils/backup_progress.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:path_provider_foundation/path_provider_foundation.dart'; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; - -final backgroundServiceProvider = Provider((ref) => BackgroundService()); - -/// Background backup service -class BackgroundService { - static const String _portNameLock = "immichLock"; - static const MethodChannel _foregroundChannel = MethodChannel('immich/foregroundChannel'); - static const MethodChannel _backgroundChannel = MethodChannel('immich/backgroundChannel'); - static const notifyInterval = Duration(milliseconds: 400); - bool _isBackgroundInitialized = false; - Completer? _cancellationToken; - bool _canceledBySystem = false; - int _wantsLockTime = 0; - bool _hasLock = false; - SendPort? _waitingIsolate; - ReceivePort? _rp; - bool _errorGracePeriodExceeded = true; - int _uploadedAssetsCount = 0; - int _assetsToUploadCount = 0; - String _lastPrintedDetailContent = ""; - String? _lastPrintedDetailTitle; - late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval); - late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate( - _updateDetailProgress, - notifyInterval, - ); - - bool get isBackgroundInitialized { - return _isBackgroundInitialized; - } - - /// Ensures that the background service is enqueued if enabled in settings - Future resumeServiceIfEnabled() async { - return await isBackgroundBackupEnabled() && await enableService(); - } - - /// Enqueues the background service - Future enableService({bool immediate = false}) async { - try { - final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; - final String title = "backup_background_service_default_notification".tr(); - final bool ok = await _foregroundChannel.invokeMethod('enable', [callback.toRawHandle(), title, immediate]); - return ok; - } catch (error) { - return false; - } - } - - /// Configures the background service - Future configureService({ - bool requireUnmetered = true, - bool requireCharging = false, - int triggerUpdateDelay = 5000, - int triggerMaxDelay = 50000, - }) async { - try { - final bool ok = await _foregroundChannel.invokeMethod('configure', [ - requireUnmetered, - requireCharging, - triggerUpdateDelay, - triggerMaxDelay, - ]); - return ok; - } catch (error) { - return false; - } - } - - /// Cancels the background service (if currently running) and removes it from work queue - Future disableService() async { - try { - final ok = await _foregroundChannel.invokeMethod('disable'); - return ok; - } catch (error) { - return false; - } - } - - /// Returns `true` if the background service is enabled - Future isBackgroundBackupEnabled() async { - try { - return await _foregroundChannel.invokeMethod("isEnabled"); - } catch (error) { - return false; - } - } - - /// Returns `true` if battery optimizations are disabled - Future isIgnoringBatteryOptimizations() async { - // iOS does not need battery optimizations enabled - if (Platform.isIOS) { - return true; - } - try { - return await _foregroundChannel.invokeMethod('isIgnoringBatteryOptimizations'); - } catch (error) { - return false; - } - } - - // Yet to be implemented - Future digestFile(String path) { - return _foregroundChannel.invokeMethod("digestFile", [path]); - } - - Future?> digestFiles(List paths) { - return _foregroundChannel.invokeListMethod("digestFiles", paths); - } - - /// Updates the notification shown by the background service - Future _updateNotification({ - String? title, - String? content, - int progress = 0, - int max = 0, - bool indeterminate = false, - bool isDetail = false, - bool onlyIfFG = false, - }) async { - try { - if (_isBackgroundInitialized) { - return _backgroundChannel.invokeMethod('updateNotification', [ - title, - content, - progress, - max, - indeterminate, - isDetail, - onlyIfFG, - ]); - } - } catch (error) { - dPrint(() => "[_updateNotification] failed to communicate with plugin"); - } - return false; - } - - /// Shows a new priority notification - Future _showErrorNotification({required String title, String? content, String? individualTag}) async { - try { - if (_isBackgroundInitialized && _errorGracePeriodExceeded) { - return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); - } - } catch (error) { - dPrint(() => "[_showErrorNotification] failed to communicate with plugin"); - } - return false; - } - - Future _clearErrorNotifications() async { - try { - if (_isBackgroundInitialized) { - return await _backgroundChannel.invokeMethod('clearErrorNotifications'); - } - } catch (error) { - dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin"); - } - return false; - } - - /// await to ensure this thread (foreground or background) has exclusive access - Future acquireLock() async { - if (_hasLock) { - dPrint(() => "WARNING: [acquireLock] called more than once"); - return true; - } - final int lockTime = Timeline.now; - _wantsLockTime = lockTime; - final ReceivePort rp = ReceivePort(_portNameLock); - _rp = rp; - final SendPort sp = rp.sendPort; - - while (!IsolateNameServer.registerPortWithName(sp, _portNameLock)) { - try { - await _checkLockReleasedWithHeartbeat(lockTime); - } catch (error) { - return false; - } - if (_wantsLockTime != lockTime) { - return false; - } - } - _hasLock = true; - rp.listen(_heartbeatListener); - return true; - } - - Future _checkLockReleasedWithHeartbeat(final int lockTime) async { - SendPort? other = IsolateNameServer.lookupPortByName(_portNameLock); - if (other != null) { - final ReceivePort tempRp = ReceivePort(); - final SendPort tempSp = tempRp.sendPort; - final bs = tempRp.asBroadcastStream(); - while (_wantsLockTime == lockTime) { - other.send(tempSp); - final dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null); - if (_wantsLockTime != lockTime) { - break; - } - if (answer == null) { - // other isolate failed to answer, assuming it exited without releasing the lock - if (other == IsolateNameServer.lookupPortByName(_portNameLock)) { - IsolateNameServer.removePortNameMapping(_portNameLock); - } - break; - } else if (answer == true) { - // other isolate released the lock - break; - } else if (answer == false) { - // other isolate is still active - } - final dynamic isFinished = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => false); - if (isFinished == true) { - break; - } - } - tempRp.close(); - } - } - - void _heartbeatListener(dynamic msg) { - if (msg is SendPort) { - _waitingIsolate = msg; - msg.send(false); - } - } - - /// releases the exclusive access lock - void releaseLock() { - _wantsLockTime = 0; - if (_hasLock) { - IsolateNameServer.removePortNameMapping(_portNameLock); - _waitingIsolate?.send(true); - _waitingIsolate = null; - _hasLock = false; - } - _rp?.close(); - _rp = null; - } - - void _setupBackgroundCallHandler() { - _backgroundChannel.setMethodCallHandler(_callHandler); - _isBackgroundInitialized = true; - _backgroundChannel.invokeMethod('initialized'); - } - - Future _callHandler(MethodCall call) async { - DartPluginRegistrant.ensureInitialized(); - if (Platform.isIOS) { - // NOTE: I'm not sure this is strictly necessary anymore, but - // out of an abundance of caution, we will keep it in until someone - // can say for sure - PathProviderFoundation.registerWith(); - } - switch (call.method) { - case "backgroundProcessing": - case "onAssetsChanged": - try { - unawaited(_clearErrorNotifications()); - - // iOS should time out after some threshold so it doesn't wait - // indefinitely and can run later - // Android is fine to wait here until the lock releases - final waitForLock = Platform.isIOS - ? acquireLock().timeout(const Duration(seconds: 5), onTimeout: () => false) - : acquireLock(); - - final bool hasAccess = await waitForLock; - if (!hasAccess) { - dPrint(() => "[_callHandler] could not acquire lock, exiting"); - return false; - } - - final translationsOk = await loadTranslations(); - if (!translationsOk) { - dPrint(() => "[_callHandler] could not load translations"); - } - - final bool ok = await _onAssetsChanged(); - return ok; - } catch (error) { - dPrint(() => error.toString()); - return false; - } finally { - releaseLock(); - } - case "systemStop": - _canceledBySystem = true; - _cancellationToken?.complete(); - _cancellationToken = null; - return true; - default: - dPrint(() => "Unknown method ${call.method}"); - return false; - } - } - - Future _onAssetsChanged() async { - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); - - final ref = ProviderContainer( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); - - await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); - - final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); - final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); - if (selectedAlbums.isEmpty) { - return true; - } - - await ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(); - - do { - final bool backupOk = await _runBackup( - ref.read(backupServiceProvider), - ref.read(appSettingsServiceProvider), - selectedAlbums, - excludedAlbums, - ); - if (backupOk) { - await Store.delete(StoreKey.backupFailedSince); - final backupAlbums = [...selectedAlbums, ...excludedAlbums]; - backupAlbums.sortBy((e) => e.id); - - final dbAlbums = await ref.read(backupAlbumRepositoryProvider).getAll(sort: BackupAlbumSort.id); - final List toDelete = []; - final List toUpsert = []; - // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state - diffSortedListsSync( - dbAlbums, - backupAlbums, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - a.lastBackup = a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(a); - return true; - }, - onlyFirst: (BackupAlbum a) => toUpsert.add(a), - onlySecond: (BackupAlbum b) => toDelete.add(b.isarId), - ); - await ref.read(backupAlbumRepositoryProvider).deleteAll(toDelete); - await ref.read(backupAlbumRepositoryProvider).updateAll(toUpsert); - } else if (Store.tryGet(StoreKey.backupFailedSince) == null) { - await Store.put(StoreKey.backupFailedSince, DateTime.now()); - return false; - } - // Android should check for new assets added while performing backup - } while (Platform.isAndroid && true == await _backgroundChannel.invokeMethod("hasContentChanged")); - return true; - } - - Future _runBackup( - BackupService backupService, - AppSettingsService settingsService, - List selectedAlbums, - List excludedAlbums, - ) async { - _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); - final bool notifyTotalProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupTotalProgress); - final bool notifySingleProgress = settingsService.getSetting(AppSettingsEnum.backgroundBackupSingleProgress); - - if (_canceledBySystem) { - return false; - } - - Set toUpload = await backupService.buildUploadCandidates(selectedAlbums, excludedAlbums); - - try { - toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); - } catch (e) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_connection_failed_message".tr(), - ), - ); - return false; - } - - if (_canceledBySystem) { - return false; - } - - if (toUpload.isEmpty) { - return true; - } - _assetsToUploadCount = toUpload.length; - _uploadedAssetsCount = 0; - unawaited( - _updateNotification( - title: "backup_background_service_in_progress_notification".tr(), - content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null, - progress: 0, - max: notifyTotalProgress ? _assetsToUploadCount : 0, - indeterminate: !notifyTotalProgress, - onlyIfFG: !notifyTotalProgress, - ), - ); - - _cancellationToken?.complete(); - _cancellationToken = Completer(); - final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - - final bool ok = await backupService.backupAsset( - toUpload, - _cancellationToken!, - pmProgressHandler: pmProgressHandler, - onSuccess: (result) => _onAssetUploaded(shouldNotify: notifyTotalProgress), - onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), - onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), - onError: _onBackupError, - isBackground: true, - ); - - if (!ok && !_cancellationToken!.isCompleted) { - unawaited( - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_backup_failed_message".tr(), - ), - ); - } - - return ok; - } - - void _onAssetUploaded({bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _uploadedAssetsCount++; - _throttledNotifiy(); - } - - void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify(progress: bytes, total: totalBytes); - } - - void _updateDetailProgress(String? title, int progress, int total) { - final String msg = total > 0 ? humanReadableBytesProgress(progress, total) : ""; - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedDetailContent || _lastPrintedDetailTitle != title) { - _lastPrintedDetailContent = msg; - _lastPrintedDetailTitle = title; - _updateNotification( - progress: total > 0 ? (progress * 1000) ~/ total : 0, - max: 1000, - isDetail: true, - title: title, - content: msg, - ); - } - } - - void _updateProgress(String? title, int progress, int total) { - _updateNotification( - progress: _uploadedAssetsCount, - max: _assetsToUploadCount, - title: title, - content: formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount), - ); - } - - void _onBackupError(ErrorUploadAsset errorAssetInfo) { - _showErrorNotification( - title: "backup_background_service_upload_failure_notification".tr( - namedArgs: {'filename': errorAssetInfo.fileName}, - ), - individualTag: errorAssetInfo.id, - ); - } - - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset, {bool shouldNotify = false}) { - if (!shouldNotify) { - return; - } - - _throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr( - namedArgs: {'filename': currentUploadAsset.fileName}, - ); - _throttledDetailNotify.progress = 0; - _throttledDetailNotify.total = 0; - } - - bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) { - final int value = appSettingsService.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod); - if (value == 0) { - return true; - } else if (value == 5) { - return false; - } - final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince); - if (failedSince == null) { - return false; - } - final Duration duration = DateTime.now().difference(failedSince); - if (value == 1) { - return duration > const Duration(minutes: 30); - } else if (value == 2) { - return duration > const Duration(hours: 2); - } else if (value == 3) { - return duration > const Duration(hours: 8); - } else if (value == 4) { - return duration > const Duration(hours: 24); - } - assert(false, "Invalid value"); - return true; - } - - Future getIOSBackupLastRun(IosBackgroundTask task) async { - if (!Platform.isIOS) { - return null; - } - // Seconds since last run - final double? lastRun = task == IosBackgroundTask.fetch - ? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime') - : await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime'); - if (lastRun == null) { - return null; - } - final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000); - return time; - } - - Future getIOSBackupNumberOfProcesses() async { - if (!Platform.isIOS) { - return 0; - } - return await _foregroundChannel.invokeMethod('numberOfBackgroundProcesses'); - } - - Future getIOSBackgroundAppRefreshEnabled() async { - if (!Platform.isIOS) { - return false; - } - return await _foregroundChannel.invokeMethod('backgroundAppRefreshEnabled'); - } -} - -enum IosBackgroundTask { fetch, processing } - -/// entry point called by Kotlin/Java code; needs to be a top-level function -@pragma('vm:entry-point') -void _nativeEntry() { - WidgetsFlutterBinding.ensureInitialized(); - DartPluginRegistrant.ensureInitialized(); - BackgroundService backgroundService = BackgroundService(); - backgroundService._setupBackgroundCallHandler(); -} diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart deleted file mode 100644 index 9b6a26be03..0000000000 --- a/mobile/lib/services/backup.service.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/repositories/upload.repository.dart'; -import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:path/path.dart' as p; -import 'package:permission_handler/permission_handler.dart' as pm; -import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; -import 'package:immich_mobile/utils/debug_print.dart'; - -final backupServiceProvider = Provider( - (ref) => BackupService( - ref.watch(apiServiceProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(assetMediaRepositoryProvider), - ), -); - -class BackupService { - final ApiService _apiService; - final Logger _log = Logger("BackupService"); - final AppSettingsService _appSetting; - final AlbumService _albumService; - final AlbumMediaRepository _albumMediaRepository; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final AssetMediaRepository _assetMediaRepository; - - BackupService( - this._apiService, - this._appSetting, - this._albumService, - this._albumMediaRepository, - this._fileMediaRepository, - this._assetRepository, - this._assetMediaRepository, - ); - - Future?> getDeviceBackupAsset() async { - final String deviceId = Store.get(StoreKey.deviceId); - - try { - return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); - } catch (e) { - dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}'); - return null; - } - } - - Future _saveDuplicatedAssetIds(List deviceAssetIds) => - _assetRepository.transaction(() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds)); - - /// Get duplicated asset id from database - Future> getDuplicatedAssetIds() async { - final duplicates = await _assetRepository.getAllDuplicatedAssetIds(); - return duplicates.toSet(); - } - - /// Returns all assets newer than the last successful backup per album - /// if `useTimeFilter` is set to true, all assets will be returned - Future> buildUploadCandidates( - List selectedBackupAlbums, - List excludedBackupAlbums, { - bool useTimeFilter = true, - }) async { - final now = DateTime.now(); - - final Set toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - if (toAdd.isEmpty) return {}; - - final Set toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedBackupAlbums, - now, - useTimeFilter: useTimeFilter, - ); - - return toAdd.difference(toRemove); - } - - Future> _fetchAssetsAndUpdateLastBackup( - List backupAlbums, - DateTime now, { - bool useTimeFilter = true, - }) async { - Set candidates = {}; - - for (final BackupAlbum backupAlbum in backupAlbums) { - final Album localAlbum; - try { - localAlbum = await _albumMediaRepository.get(backupAlbum.id); - } on StateError { - // the album no longer exists - continue; - } - - if (useTimeFilter && localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) { - continue; - } - final List assets; - try { - assets = await _albumMediaRepository.getAssets( - backupAlbum.id, - modifiedFrom: useTimeFilter - ? - // subtract 2 seconds to prevent missing assets due to rounding issues - backupAlbum.lastBackup.subtract(const Duration(seconds: 2)) - : null, - modifiedUntil: useTimeFilter ? now : null, - ); - } on StateError { - // either there are no assets matching the filter criteria OR the album no longer exists - continue; - } - - // Add album's name to the asset info - for (final asset in assets) { - List albumNames = [localAlbum.name]; - - final existingAsset = candidates.firstWhereOrNull((candidate) => candidate.asset.localId == asset.localId); - - if (existingAsset != null) { - albumNames.addAll(existingAsset.albumNames); - candidates.remove(existingAsset); - } - - candidates.add(BackupCandidate(asset: asset, albumNames: albumNames)); - } - - backupAlbum.lastBackup = now; - } - - return candidates; - } - - /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets(Set candidates) async { - if (candidates.isEmpty) { - return candidates; - } - - final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId)); - - if (candidates.isEmpty) { - return candidates; - } - - final Set existing = {}; - try { - final String deviceId = Store.get(StoreKey.deviceId); - final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( - CheckExistingAssetsDto(deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(), deviceId: deviceId), - ); - if (duplicates != null) { - existing.addAll(duplicates.existingIds); - } - } on ApiException { - // workaround for older server versions or when checking for too many assets at once - final List? allAssetsInDatabase = await getDeviceBackupAsset(); - if (allAssetsInDatabase != null) { - existing.addAll(allAssetsInDatabase); - } - } - - if (existing.isNotEmpty) { - candidates.removeWhere((c) => existing.contains(c.asset.localId)); - } - - return candidates; - } - - Future _checkPermissions() async { - if (Platform.isAndroid && !(await pm.Permission.accessMediaLocation.status).isGranted) { - // double check that permission is granted here, to guard against - // uploading corrupt assets without EXIF information - _log.warning( - "Media location permission is not granted. " - "Cannot access original assets for backup.", - ); - - return false; - } - - // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS - if (Platform.isIOS) { - await _fileMediaRepository.requestExtendedPermissions(); - } - - return true; - } - - /// Upload images before video assets for background tasks - /// these are further sorted by using their creation date - List _sortPhotosFirst(List candidates) { - return candidates.sorted((a, b) { - final cmp = a.asset.type.index - b.asset.type.index; - if (cmp != 0) return cmp; - return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt); - }); - } - - Future backupAsset( - Iterable assets, - Completer cancelToken, { - bool isBackground = false, - PMProgressHandler? pmProgressHandler, - required void Function(SuccessUploadAsset result) onSuccess, - required void Function(int bytes, int totalBytes) onProgress, - required void Function(CurrentUploadAsset asset) onCurrentAsset, - required void Function(ErrorUploadAsset error) onError, - }) async { - final bool isIgnoreIcloudAssets = _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); - final String deviceId = Store.get(StoreKey.deviceId); - final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - final List duplicatedAssetIds = []; - bool anyErrors = false; - - final hasPermission = await _checkPermissions(); - if (!hasPermission) { - return false; - } - - List candidates = assets.toList(); - if (isBackground) { - candidates = _sortPhotosFirst(candidates); - } - - for (final candidate in candidates) { - final Asset asset = candidate.asset; - File? file; - File? livePhotoFile; - - try { - final isAvailableLocally = await asset.local!.isLocallyAvailable(isOrigin: true); - - // Handle getting files from iCloud - if (!isAvailableLocally && Platform.isIOS) { - // Skip iCloud assets if the user has disabled this feature - if (isIgnoreIcloudAssets) { - continue; - } - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: asset.fileName, - fileType: _getAssetType(asset.type), - iCloudAsset: true, - ), - ); - - file = await asset.local!.loadFile(progressHandler: pmProgressHandler); - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.loadFile(withSubtype: true, progressHandler: pmProgressHandler); - } - } else { - file = await asset.local!.originFile.timeout(const Duration(seconds: 5)); - - if (asset.local!.isLivePhoto) { - livePhotoFile = await asset.local!.originFileWithSubtype.timeout(const Duration(seconds: 5)); - } - } - - if (file != null) { - String? originalFileName = await _assetMediaRepository.getOriginalFilename(asset.localId!); - originalFileName ??= asset.fileName; - - if (asset.local!.isLivePhoto) { - if (livePhotoFile == null) { - _log.warning("Failed to obtain motion part of the livePhoto - $originalFileName"); - } - } - - final fileStream = file.openRead(); - final assetRawUploadData = MultipartFile( - "assetData", - fileStream, - file.lengthSync(), - filename: originalFileName, - ); - - final baseRequest = ProgressMultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/assets'), - abortTrigger: cancelToken.future, - onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), - ); - - baseRequest.fields['deviceAssetId'] = asset.localId!; - baseRequest.fields['deviceId'] = deviceId; - baseRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String(); - baseRequest.fields['fileModifiedAt'] = asset.fileModifiedAt.toUtc().toIso8601String(); - baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); - baseRequest.fields['duration'] = asset.duration.toString(); - baseRequest.files.add(assetRawUploadData); - - onCurrentAsset( - CurrentUploadAsset( - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt.year == 1970 ? asset.fileModifiedAt : asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(asset.type), - fileSize: file.lengthSync(), - iCloudAsset: false, - ), - ); - - String? livePhotoVideoId; - if (asset.local!.isLivePhoto && livePhotoFile != null) { - livePhotoVideoId = await uploadLivePhotoVideo(originalFileName, livePhotoFile, baseRequest, cancelToken); - } - - if (livePhotoVideoId != null) { - baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; - } - - final response = await NetworkRepository.client.send(baseRequest); - - final responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - final errorMessage = error['message'] ?? error['error']; - - dPrint( - () => - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", - ); - - onError( - ErrorUploadAsset( - asset: asset, - id: asset.localId!, - fileCreatedAt: asset.fileCreatedAt, - fileName: originalFileName, - fileType: _getAssetType(candidate.asset.type), - errorMessage: errorMessage, - ), - ); - - if (errorMessage == "Quota has been exceeded!") { - anyErrors = true; - break; - } - - continue; - } - - bool isDuplicate = false; - if (response.statusCode == 200) { - isDuplicate = true; - duplicatedAssetIds.add(asset.localId!); - } - - onSuccess( - SuccessUploadAsset( - candidate: candidate, - remoteAssetId: responseBody['id'] as String, - isDuplicate: isDuplicate, - ), - ); - - if (shouldSyncAlbums) { - await _albumService.syncUploadAlbums(candidate.albumNames, [responseBody['id'] as String]); - } - } - } on RequestAbortedException { - dPrint(() => "Backup was cancelled by the user"); - anyErrors = true; - break; - } catch (error, stackTrace) { - dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace"); - anyErrors = true; - continue; - } finally { - if (Platform.isIOS) { - try { - await file?.delete(); - await livePhotoFile?.delete(); - } catch (e) { - dPrint(() => "ERROR deleting file: ${e.toString()}"); - } - } - } - } - - if (duplicatedAssetIds.isNotEmpty) { - await _saveDuplicatedAssetIds(duplicatedAssetIds); - } - - return !anyErrors; - } - - Future uploadLivePhotoVideo( - String originalFileName, - File? livePhotoVideoFile, - MultipartRequest baseRequest, - Completer cancelToken, - ) async { - if (livePhotoVideoFile == null) { - return null; - } - final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoVideoFile.path)); - final fileStream = livePhotoVideoFile.openRead(); - final livePhotoRawUploadData = MultipartFile( - "assetData", - fileStream, - livePhotoVideoFile.lengthSync(), - filename: livePhotoTitle, - ); - final livePhotoReq = ProgressMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future) - ..headers.addAll(baseRequest.headers) - ..fields.addAll(baseRequest.fields); - - livePhotoReq.files.add(livePhotoRawUploadData); - - var response = await NetworkRepository.client.send(livePhotoReq); - - var responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - - dPrint( - () => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", - ); - } - - return responseBody.containsKey('id') ? responseBody['id'] : null; - } - - String _getAssetType(AssetType assetType) => switch (assetType) { - AssetType.audio => "AUDIO", - AssetType.image => "IMAGE", - AssetType.video => "VIDEO", - AssetType.other => "OTHER", - }; -} diff --git a/mobile/lib/services/backup_album.service.dart b/mobile/lib/services/backup_album.service.dart deleted file mode 100644 index ef9d1031de..0000000000 --- a/mobile/lib/services/backup_album.service.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; - -final backupAlbumServiceProvider = Provider((ref) { - return BackupAlbumService(ref.watch(backupAlbumRepositoryProvider)); -}); - -class BackupAlbumService { - final BackupAlbumRepository _backupAlbumRepository; - - const BackupAlbumService(this._backupAlbumRepository); - - Future> getAll({BackupAlbumSort? sort}) { - return _backupAlbumRepository.getAll(sort: sort); - } - - Future> getIdsBySelection(BackupSelection backup) { - return _backupAlbumRepository.getIdsBySelection(backup); - } - - Future> getAllBySelection(BackupSelection backup) { - return _backupAlbumRepository.getAllBySelection(backup); - } - - Future deleteAll(List ids) { - return _backupAlbumRepository.deleteAll(ids); - } - - Future updateAll(List backupAlbums) { - return _backupAlbumRepository.updateAll(backupAlbums); - } -} diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart deleted file mode 100644 index 2efd52cc81..0000000000 --- a/mobile/lib/services/backup_verification.service.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/bootstrap.dart'; -import 'package:immich_mobile/utils/diff.dart'; - -/// Finds duplicates originating from missing EXIF information -class BackupVerificationService { - final UserService _userService; - final FileMediaRepository _fileMediaRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - - const BackupVerificationService( - this._userService, - this._fileMediaRepository, - this._assetRepository, - this._exifInfoRepository, - ); - - /// Returns at most [limit] assets that were backed up without exif - Future> findWronglyBackedUpAssets({int limit = 100}) async { - final owner = _userService.getMyUser().id; - final List onlyLocal = await _assetRepository.getAll(ownerId: owner, state: AssetState.local, limit: limit); - final List remoteMatches = await _assetRepository.getMatches( - assets: onlyLocal, - ownerId: owner, - state: AssetState.remote, - limit: limit, - ); - final List localMatches = await _assetRepository.getMatches( - assets: remoteMatches, - ownerId: owner, - state: AssetState.local, - limit: limit, - ); - - final List deleteCandidates = [], originals = []; - - await diffSortedLists( - remoteMatches, - localMatches, - compare: (a, b) => a.fileName.compareTo(b.fileName), - both: (a, b) async { - a.exifInfo = await _exifInfoRepository.get(a.id); - deleteCandidates.add(a); - originals.add(b); - return false; - }, - onlyFirst: (a) {}, - onlySecond: (b) {}, - ); - final isolateToken = ServicesBinding.rootIsolateToken!; - final List toDelete; - if (deleteCandidates.length > 10) { - // performs 2 checks in parallel for a nice speedup - final half = deleteCandidates.length ~/ 2; - final lower = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(0, half), - originals: originals.slice(0, half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - final upper = compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates.slice(half), - originals: originals.slice(half), - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - toDelete = await lower + await upper; - } else { - toDelete = await compute(_computeSaveToDelete, ( - deleteCandidates: deleteCandidates, - originals: originals, - endpoint: Store.get(StoreKey.serverEndpoint), - rootIsolateToken: isolateToken, - fileMediaRepository: _fileMediaRepository, - )); - } - return toDelete; - } - - static Future> _computeSaveToDelete( - ({ - List deleteCandidates, - List originals, - String endpoint, - RootIsolateToken rootIsolateToken, - FileMediaRepository fileMediaRepository, - }) - tuple, - ) async { - assert(tuple.deleteCandidates.length == tuple.originals.length); - final List result = []; - BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); - await tuple.fileMediaRepository.enableBackgroundAccess(); - final ApiService apiService = ApiService(); - apiService.setEndpoint(tuple.endpoint); - for (int i = 0; i < tuple.deleteCandidates.length; i++) { - if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) { - result.add(tuple.deleteCandidates[i]); - } - } - return result; - } - - static Future _compareAssets(Asset remote, Asset local, ApiService apiService) async { - if (remote.checksum == local.checksum) return false; - ExifInfo? exif = remote.exifInfo; - if (exif != null && exif.latitude != null) return false; - if (exif == null || exif.fileSize == null) { - final dto = await apiService.assetsApi.getAssetInfo(remote.remoteId!); - if (dto != null && dto.exifInfo != null) { - exif = ExifDtoConverter.fromDto(dto.exifInfo!); - } - } - final file = await local.local!.originFile; - if (exif != null && file != null && exif.fileSize != null) { - final origSize = await file.length(); - if (exif.fileSize! == origSize || exif.fileSize! != origSize) { - final latLng = await local.local!.latlngAsync(); - - if (exif.latitude == null && - latLng.latitude != null && - (remote.fileCreatedAt.isAtSameMomentAs(local.fileCreatedAt) || - remote.fileModifiedAt.isAtSameMomentAs(local.fileModifiedAt) || - _sameExceptTimeZone(remote.fileCreatedAt, local.fileCreatedAt))) { - if (remote.type == AssetType.video) { - // it's very unlikely that a video of same length, filesize, name - // and date is wrong match. Cannot easily compare videos anyway - return true; - } - - // for images: make sure they are pixel-wise identical - // (skip first few KBs containing metadata) - final Uint64List localImage = _fakeDecodeImg(await file.readAsBytes()); - final res = await apiService.assetsApi.downloadAssetWithHttpInfo(remote.remoteId!); - final Uint64List remoteImage = _fakeDecodeImg(res.bodyBytes); - - final eq = const ListEquality().equals(remoteImage, localImage); - return eq; - } - } - } - - return false; - } - - static Uint64List _fakeDecodeImg(Uint8List bytes) { - const headerLength = 131072; // assume header is at most 128 KB - final start = bytes.length < headerLength * 2 ? (bytes.length ~/ (4 * 8)) * 8 : headerLength; - return bytes.buffer.asUint64List(start); - } - - static bool _sameExceptTimeZone(DateTime a, DateTime b) { - final ms = a.isAfter(b) - ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch - : b.millisecondsSinceEpoch - a.microsecondsSinceEpoch; - final x = ms / (1000 * 60 * 30); - final y = ms ~/ (1000 * 60 * 30); - return y.toDouble() == x && y < 24; - } -} - -final backupVerificationServiceProvider = Provider( - (ref) => BackupVerificationService( - ref.watch(userServiceProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ), -); diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 9d2bdbe4a0..5ff0fa8a4d 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -7,10 +7,7 @@ import 'package:immich_mobile/domain/services/memory.service.dart'; import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @@ -18,19 +15,9 @@ import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/memory.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; final deepLinkServiceProvider = Provider( (ref) => DeepLinkService( - ref.watch(memoryServiceProvider), - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(currentAssetProvider.notifier), - ref.watch(currentAlbumProvider.notifier), - // Below is used for beta timeline ref.watch(timelineFactoryProvider), ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), @@ -41,14 +28,6 @@ final deepLinkServiceProvider = Provider( ); class DeepLinkService { - /// TODO: Remove this when beta is default - final MemoryService _memoryService; - final AssetService _assetService; - final AlbumService _albumService; - final CurrentAsset _currentAsset; - final CurrentAlbum _currentAlbum; - - /// Used for beta timeline final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; @@ -58,11 +37,6 @@ class DeepLinkService { final UserDto? _currentUser; const DeepLinkService( - this._memoryService, - this._assetService, - this._albumService, - this._currentAsset, - this._currentAlbum, this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, @@ -75,7 +49,7 @@ class DeepLinkService { return DeepLink([ // we need something to segue back to if the app was cold started // TODO: use MainTimelineRoute this when beta is default - if (isColdStart) (Store.isBetaTimelineEnabled) ? const TabShellRoute() : const PhotosRoute(), + if (isColdStart) const TabShellRoute(), route, ]); } @@ -138,95 +112,52 @@ class DeepLinkService { } Future _buildMemoryDeepLink(String? memoryId) async { - if (Store.isBetaTimelineEnabled) { - List memories = []; + List memories = []; - if (memoryId == null) { - if (_currentUser == null) { - return null; - } - - memories = await _betaMemoryService.getMemoryLane(_currentUser.id); - } else { - final memory = await _betaMemoryService.get(memoryId); - if (memory != null) { - memories = [memory]; - } - } - - if (memories.isEmpty) { + if (memoryId == null) { + if (_currentUser == null) { return null; } - return DriftMemoryRoute(memories: memories, memoryIndex: 0); + memories = await _betaMemoryService.getMemoryLane(_currentUser.id); } else { - // TODO: Remove this when beta is default - if (memoryId == null) { - return null; + final memory = await _betaMemoryService.get(memoryId); + if (memory != null) { + memories = [memory]; } - final memory = await _memoryService.getMemoryById(memoryId); - - if (memory == null) { - return null; - } - - return MemoryRoute(memories: [memory], memoryIndex: 0); } - } - Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { - if (Store.isBetaTimelineEnabled) { - final asset = await _betaAssetService.getRemoteAsset(assetId); - if (asset == null) { - return null; - } - - AssetViewer.setAsset(ref, asset); - return AssetViewerRoute( - initialIndex: 0, - timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), - ); - } else { - // TODO: Remove this when beta is default - final asset = await _assetService.getAssetByRemoteId(assetId); - if (asset == null) { - return null; - } - - _currentAsset.set(asset); - final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.auto); - - return GalleryViewerRoute(renderList: renderList, initialIndex: 0, heroOffset: 0, showStack: true); - } - } - - Future _buildAlbumDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled) { - final album = await _betaRemoteAlbumService.get(albumId); - - if (album == null) { - return null; - } - - return RemoteAlbumRoute(album: album); - } else { - // TODO: Remove this when beta is default - final album = await _albumService.getAlbumByRemoteId(albumId); - - if (album == null) { - return null; - } - - _currentAlbum.set(album); - return AlbumViewerRoute(albumId: album.id); - } - } - - Future _buildActivityDeepLink(String albumId) async { - if (Store.isBetaTimelineEnabled == false) { + if (memories.isEmpty) { return null; } + return DriftMemoryRoute(memories: memories, memoryIndex: 0); + } + + Future _buildAssetDeepLink(String assetId, WidgetRef ref) async { + final asset = await _betaAssetService.getRemoteAsset(assetId); + if (asset == null) { + return null; + } + + AssetViewer.setAsset(ref, asset); + return AssetViewerRoute( + initialIndex: 0, + timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), + ); + } + + Future _buildAlbumDeepLink(String albumId) async { + final album = await _betaRemoteAlbumService.get(albumId); + + if (album == null) { + return null; + } + + return RemoteAlbumRoute(album: album); + } + + Future _buildActivityDeepLink(String albumId) async { final album = await _betaRemoteAlbumService.get(albumId); if (album == null || album.isActivityEnabled == false) { @@ -237,10 +168,6 @@ class DeepLinkService { } Future _buildPeopleDeepLink(String personId) async { - if (Store.isBetaTimelineEnabled == false) { - return null; - } - final person = await _betaPeopleService.get(personId); if (person == null) { diff --git a/mobile/lib/services/device.service.dart b/mobile/lib/services/device.service.dart deleted file mode 100644 index 50a0d93b24..0000000000 --- a/mobile/lib/services/device.service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_udid/flutter_udid.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; - -final deviceServiceProvider = Provider((ref) => const DeviceService()); - -class DeviceService { - const DeviceService(); - - createDeviceId() { - return FlutterUdid.consistentUdid; - } - - /// Returns the device ID from local storage or creates a new one if not found. - /// - /// This method first attempts to retrieve the device ID from the local store using - /// [StoreKey.deviceId]. If no device ID is found (returns null), it generates a - /// new device ID by calling [createDeviceId]. - /// - /// Returns a [String] representing the device's unique identifier. - String getDeviceId() { - return Store.tryGet(StoreKey.deviceId) ?? createDeviceId(); - } -} diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index 8e810ced2a..3f2c36fa7e 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -3,14 +3,9 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; final downloadServiceProvider = Provider( @@ -54,7 +49,7 @@ class DownloadService { final title = task.filename; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; try { - final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile( + final resultAsset = await _fileMediaRepository.saveImageWithFile( filePath, title: title, relativePath: relativePath, @@ -76,7 +71,7 @@ class DownloadService { final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final file = File(filePath); try { - final Asset? resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); + final resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath); return resultAsset != null; } catch (error, stack) { _log.severe("Error saving video", error, stack); @@ -136,62 +131,6 @@ class DownloadService { Future cancelDownload(String id) async { return await FileDownloader().cancelTaskWithId(id); } - - Future> downloadAll(List assets) async { - return await _downloadRepository.downloadAll(assets.expand(_createDownloadTasks).toList()); - } - - Future download(Asset asset) async { - final tasks = _createDownloadTasks(asset); - await _downloadRepository.downloadAll(tasks); - } - - List _createDownloadTasks(Asset asset) { - if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.image, id: asset.remoteId!).toJson(), - ), - _buildDownloadTask( - asset.livePhotoVideoId!, - asset.fileName.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), - group: kDownloadGroupLivePhoto, - metadata: LivePhotosMetadata(part: LivePhotosPart.video, id: asset.remoteId!).toJson(), - ), - ]; - } - - if (asset.remoteId == null) { - return []; - } - - return [ - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: asset.isImage ? kDownloadGroupImage : kDownloadGroupVideo, - ), - ]; - } - - DownloadTask _buildDownloadTask(String id, String filename, {String? group, String? metadata}) { - final path = r'/assets/{id}/original'.replaceAll('{id}', id); - final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headers = ApiService.getRequestHeaders(); - - return DownloadTask( - taskId: id, - url: serverEndpoint + path, - headers: headers, - filename: filename, - updates: Updates.statusAndProgress, - group: group ?? '', - metaData: metadata ?? '', - ); - } } TaskRecord _findTaskRecord(List records, String livePhotosId, LivePhotosPart part) { diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart deleted file mode 100644 index fe7358fce6..0000000000 --- a/mobile/lib/services/entity.service.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; - -class EntityService { - final AssetRepository _assetRepository; - final IsarUserRepository _isarUserRepository; - const EntityService(this._assetRepository, this._isarUserRepository); - - Future fillAlbumWithDatabaseEntities(Album album) async { - final ownerId = album.ownerId; - if (ownerId != null) { - // replace owner with user from database - final user = await _isarUserRepository.getByUserId(ownerId); - album.owner.value = user == null ? null : User.fromDto(user); - } - final thumbnailAssetId = album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId; - if (thumbnailAssetId != null) { - // set thumbnail with asset from database - album.thumbnail.value = await _assetRepository.getByRemoteId(thumbnailAssetId); - } - if (album.remoteUsers.isNotEmpty) { - // replace all users with users from database - final users = await _isarUserRepository.getByUserIds(album.remoteUsers.map((user) => user.id).toList()); - album.sharedUsers.clear(); - album.sharedUsers.addAll(users.nonNulls.map(User.fromDto)); - album.shared = true; - } - if (album.remoteAssets.isNotEmpty) { - // replace all assets with assets from database - final assets = await _assetRepository.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!)); - album.assets.clear(); - album.assets.addAll(assets); - } - return album; - } -} - -final entityServiceProvider = Provider( - (ref) => EntityService(ref.watch(assetRepositoryProvider), ref.watch(userRepositoryProvider)), -); diff --git a/mobile/lib/services/etag.service.dart b/mobile/lib/services/etag.service.dart deleted file mode 100644 index 00eb83fcea..0000000000 --- a/mobile/lib/services/etag.service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; - -final etagServiceProvider = Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); - -class ETagService { - final ETagRepository _eTagRepository; - - const ETagService(this._eTagRepository); - - Future clearTable() { - return _eTagRepository.clearTable(); - } -} diff --git a/mobile/lib/services/exif.service.dart b/mobile/lib/services/exif.service.dart deleted file mode 100644 index 57f793b21e..0000000000 --- a/mobile/lib/services/exif.service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; - -final exifServiceProvider = Provider((ref) => ExifService(ref.watch(exifRepositoryProvider))); - -class ExifService { - final IsarExifRepository _exifInfoRepository; - - const ExifService(this._exifInfoRepository); - - Future clearTable() { - return _exifInfoRepository.deleteAll(); - } -} diff --git a/mobile/lib/services/folder.service.dart b/mobile/lib/services/folder.service.dart index 91fb455110..bf7590ce54 100644 --- a/mobile/lib/services/folder.service.dart +++ b/mobile/lib/services/folder.service.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/repositories/folder_api.repository.dart'; @@ -76,7 +76,7 @@ class FolderService { return RootFolder(subfolders: rootSubfolders, path: '/'); } - Future> getFolderAssets(RootFolder folder, SortOrder order) async { + Future> getFolderAssets(RootFolder folder, SortOrder order) async { try { if (folder is RecursiveFolder) { String fullPath = folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}'; @@ -84,9 +84,9 @@ class FolderService { var result = await _folderApiRepository.getAssetsForPath(fullPath); if (order == SortOrder.desc) { - result.sort((a, b) => b.fileCreatedAt.compareTo(a.fileCreatedAt)); + result.sort((a, b) => b.createdAt.compareTo(a.createdAt)); } else { - result.sort((a, b) => a.fileCreatedAt.compareTo(b.fileCreatedAt)); + result.sort((a, b) => a.createdAt.compareTo(b.createdAt)); } return result; diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart deleted file mode 100644 index 9d1f4e51e8..0000000000 --- a/mobile/lib/services/hash.service.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:logging/logging.dart'; - -class HashService { - HashService({ - required IsarDeviceAssetRepository deviceAssetRepository, - required BackgroundService backgroundService, - this.batchSizeLimit = kBatchHashSizeLimit, - int? batchFileLimit, - }) : _deviceAssetRepository = deviceAssetRepository, - _backgroundService = backgroundService, - batchFileLimit = batchFileLimit ?? kBatchHashFileLimit; - - final IsarDeviceAssetRepository _deviceAssetRepository; - final BackgroundService _backgroundService; - final int batchSizeLimit; - final int batchFileLimit; - final _log = Logger('HashService'); - - /// Processes a list of local [Asset]s, storing their hash and returning only those - /// that were successfully hashed. Hashes are looked up in a DB table - /// [DeviceAsset] by local id. Only missing entries are newly hashed and added to the DB table. - Future> hashAssets(List assets) async { - assets.sort(Asset.compareByLocalId); - - // Get and sort DB entries - guaranteed to be a subset of assets - final hashesInDB = await _deviceAssetRepository.getByIds(assets.map((a) => a.localId!).toList()); - hashesInDB.sort((a, b) => a.assetId.compareTo(b.assetId)); - - int dbIndex = 0; - int bytesProcessed = 0; - final hashedAssets = []; - final toBeHashed = <_AssetPath>[]; - final toBeDeleted = []; - - for (int assetIndex = 0; assetIndex < assets.length; assetIndex++) { - final asset = assets[assetIndex]; - DeviceAsset? matchingDbEntry; - - if (dbIndex < hashesInDB.length) { - final deviceAsset = hashesInDB[dbIndex]; - if (deviceAsset.assetId == asset.localId) { - matchingDbEntry = deviceAsset; - dbIndex++; - } - } - - if (matchingDbEntry != null && - matchingDbEntry.hash.isNotEmpty && - matchingDbEntry.modifiedTime.isAtSameMomentAs(asset.fileModifiedAt)) { - // Reuse the existing hash - hashedAssets.add(asset.copyWith(checksum: base64.encode(matchingDbEntry.hash))); - continue; - } - - final file = await _tryGetAssetFile(asset); - if (file == null) { - // Can't access file, delete any DB entry - if (matchingDbEntry != null) { - toBeDeleted.add(matchingDbEntry.assetId); - } - continue; - } - - bytesProcessed += await file.length(); - toBeHashed.add(_AssetPath(asset: asset, path: file.path)); - - if (_shouldProcessBatch(toBeHashed.length, bytesProcessed)) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - toBeHashed.clear(); - toBeDeleted.clear(); - bytesProcessed = 0; - } - } - assert(dbIndex == hashesInDB.length, "All hashes should've been processed"); - - // Process any remaining files - if (toBeHashed.isNotEmpty) { - hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted)); - } - - // Clean up deleted references - if (toBeDeleted.isNotEmpty) { - await _deviceAssetRepository.deleteIds(toBeDeleted); - } - - return hashedAssets; - } - - bool _shouldProcessBatch(int assetCount, int bytesProcessed) => - assetCount >= batchFileLimit || bytesProcessed >= batchSizeLimit; - - Future _tryGetAssetFile(Asset asset) async { - try { - final file = await asset.local!.originFile; - if (file == null) { - _log.warning( - "Failed to get file for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - ); - return null; - } - return file; - } catch (error, stackTrace) { - _log.warning( - "Error getting file to hash for asset ${asset.localId ?? ''}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping", - error, - stackTrace, - ); - return null; - } - } - - /// Processes a batch of files and returns a list of successfully hashed assets after saving - /// them in [DeviceAssetToHash] for future retrieval - Future> _processBatch(List<_AssetPath> toBeHashed, List toBeDeleted) async { - _log.info("Hashing ${toBeHashed.length} files"); - final hashes = await _hashFiles(toBeHashed.map((e) => e.path).toList()); - assert( - hashes.length == toBeHashed.length, - "Number of Hashes returned from platform should be the same as the input", - ); - - final hashedAssets = []; - final toBeAdded = []; - - for (final (index, hash) in hashes.indexed) { - final asset = toBeHashed.elementAtOrNull(index)?.asset; - if (asset != null && hash?.length == 20) { - hashedAssets.add(asset.copyWith(checksum: base64.encode(hash!))); - toBeAdded.add(DeviceAsset(assetId: asset.localId!, hash: hash, modifiedTime: asset.fileModifiedAt)); - } else { - _log.warning("Failed to hash file ${asset?.localId ?? ''}"); - if (asset != null) { - toBeDeleted.add(asset.localId!); - } - } - } - - // Update the DB for future retrieval - await _deviceAssetRepository.transaction(() async { - await _deviceAssetRepository.updateAll(toBeAdded); - await _deviceAssetRepository.deleteIds(toBeDeleted); - }); - - _log.fine("Hashed ${hashedAssets.length}/${toBeHashed.length} assets"); - return hashedAssets; - } - - /// Hashes the given files and returns a list of the same length. - /// Files that could not be hashed will have a `null` value - Future> _hashFiles(List paths) async { - try { - final hashes = await _backgroundService.digestFiles(paths); - if (hashes != null) { - return hashes; - } - _log.severe("Hashing ${paths.length} files failed"); - } catch (e, s) { - _log.severe("Error occurred while hashing assets", e, s); - } - return List.filled(paths.length, null); - } -} - -class _AssetPath { - final Asset asset; - final String path; - - const _AssetPath({required this.asset, required this.path}); - - _AssetPath copyWith({Asset? asset, String? path}) { - return _AssetPath(asset: asset ?? this.asset, path: path ?? this.path); - } -} - -final hashServiceProvider = Provider( - (ref) => HashService( - deviceAssetRepository: ref.watch(deviceAssetRepositoryProvider), - backgroundService: ref.watch(backgroundServiceProvider), - ), -); diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart deleted file mode 100644 index bf85f4a9a9..0000000000 --- a/mobile/lib/services/local_notification.service.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/notification_permission.provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final localNotificationService = Provider( - (ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref), -); - -class LocalNotificationService { - final FlutterLocalNotificationsPlugin _localNotificationPlugin = FlutterLocalNotificationsPlugin(); - final PermissionStatus _permissionStatus; - final Ref ref; - - LocalNotificationService(this._permissionStatus, this.ref); - - static const manualUploadNotificationID = 4; - static const manualUploadDetailedNotificationID = 5; - static const manualUploadChannelName = 'Manual Asset Upload'; - static const manualUploadChannelID = 'immich/manualUpload'; - static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed'; - static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed'; - static const cancelUploadActionID = 'cancel_upload'; - - Future setup() async { - const androidSetting = AndroidInitializationSettings('@drawable/notification_icon'); - const iosSetting = DarwinInitializationSettings(); - - const initSettings = InitializationSettings(android: androidSetting, iOS: iosSetting); - - await _localNotificationPlugin.initialize( - initSettings, - onDidReceiveNotificationResponse: _onDidReceiveForegroundNotificationResponse, - ); - } - - Future _showOrUpdateNotification( - int id, - String title, - String body, - AndroidNotificationDetails androidNotificationDetails, - DarwinNotificationDetails iosNotificationDetails, - ) async { - final notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails); - - if (_permissionStatus == PermissionStatus.granted) { - await _localNotificationPlugin.show(id, title, body, notificationDetails); - } - } - - Future closeNotification(int id) { - return _localNotificationPlugin.cancel(id); - } - - Future showOrUpdateManualUploadStatus( - String title, - String body, { - bool? isDetailed, - bool? presentBanner, - bool? showActions, - int? maxProgress, - int? progress, - }) { - var notificationlId = manualUploadNotificationID; - var androidChannelID = manualUploadChannelID; - var androidChannelName = manualUploadChannelName; - // Separate Notification for Info/Alerts and Progress - if (isDetailed != null && isDetailed) { - notificationlId = manualUploadDetailedNotificationID; - androidChannelID = manualUploadDetailedChannelID; - androidChannelName = manualUploadChannelNameDetailed; - } - // Progress notification - final androidNotificationDetails = (maxProgress != null && progress != null) - ? AndroidNotificationDetails( - androidChannelID, - androidChannelName, - ticker: title, - showProgress: true, - onlyAlertOnce: true, - maxProgress: maxProgress, - progress: progress, - indeterminate: false, - playSound: false, - priority: Priority.low, - importance: Importance.low, - ongoing: true, - actions: (showActions ?? false) - ? [ - const AndroidNotificationAction(cancelUploadActionID, 'Cancel', showsUserInterface: true), - ] - : null, - ) - // Non-progress notification - : AndroidNotificationDetails(androidChannelID, androidChannelName, playSound: false); - - final iosNotificationDetails = DarwinNotificationDetails( - presentBadge: true, - presentList: true, - presentBanner: presentBanner, - ); - - return _showOrUpdateNotification(notificationlId, title, body, androidNotificationDetails, iosNotificationDetails); - } - - void _onDidReceiveForegroundNotificationResponse(NotificationResponse notificationResponse) { - // Handle notification actions - switch (notificationResponse.actionId) { - case cancelUploadActionID: - { - dPrint(() => "User cancelled manual upload operation"); - ref.read(manualUploadProvider.notifier).cancelBackup(); - } - } - } -} diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart deleted file mode 100644 index e485bb0957..0000000000 --- a/mobile/lib/services/memory.service.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; - -final memoryServiceProvider = StateProvider((ref) { - return MemoryService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)); -}); - -class MemoryService { - final log = Logger("MemoryService"); - - final ApiService _apiService; - final AssetRepository _assetRepository; - - MemoryService(this._apiService, this._assetRepository); - - Future?> getMemoryLane() async { - try { - final now = DateTime.now(); - final data = await _apiService.memoriesApi.searchMemories( - for_: DateTime.utc(now.year, now.month, now.day, 0, 0, 0), - ); - - if (data == null) { - return null; - } - - List memories = []; - - for (final memory in data) { - final dbAssets = await _assetRepository.getAllByRemoteId(memory.assets.map((e) => e.id)); - final yearsAgo = now.year - memory.data.year; - if (dbAssets.isNotEmpty) { - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - memories.add(Memory(title: title, assets: dbAssets)); - } - } - - return memories.isNotEmpty ? memories : null; - } catch (error, stack) { - log.severe("Cannot get memories", error, stack); - return null; - } - } - - Future getMemoryById(String id) async { - try { - final memoryResponse = await _apiService.memoriesApi.getMemory(id); - - if (memoryResponse == null) { - return null; - } - final dbAssets = await _assetRepository.getAllByRemoteId(memoryResponse.assets.map((e) => e.id)); - if (dbAssets.isEmpty) { - log.warning("No assets found for memory with ID: $id"); - return null; - } - final yearsAgo = DateTime.now().year - memoryResponse.data.year; - final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()}); - - return Memory(title: title, assets: dbAssets); - } catch (error, stack) { - log.severe("Cannot get memory with ID: $id", error, stack); - return null; - } - } -} diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart deleted file mode 100644 index b8e5ae9a4d..0000000000 --- a/mobile/lib/services/partner.service.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:logging/logging.dart'; - -final partnerServiceProvider = Provider( - (ref) => PartnerService( - ref.watch(partnerApiRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ), -); - -class PartnerService { - final PartnerApiRepository _partnerApiRepository; - final PartnerRepository _partnerRepository; - final IsarUserRepository _isarUserRepository; - final Logger _log = Logger("PartnerService"); - - PartnerService(this._partnerApiRepository, this._isarUserRepository, this._partnerRepository); - - Future> getSharedWith() async { - return _partnerRepository.getSharedWith(); - } - - Future> getSharedBy() async { - return _partnerRepository.getSharedBy(); - } - - Stream> watchSharedWith() { - return _partnerRepository.watchSharedWith(); - } - - Stream> watchSharedBy() { - return _partnerRepository.watchSharedBy(); - } - - Future removePartner(UserDto partner) async { - try { - await _partnerApiRepository.delete(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: false)); - } catch (e) { - _log.warning("Failed to remove partner ${partner.id}", e); - return false; - } - return true; - } - - Future addPartner(UserDto partner) async { - try { - await _partnerApiRepository.create(partner.id); - await _isarUserRepository.update(partner.copyWith(isPartnerSharedBy: true)); - return true; - } catch (e) { - _log.warning("Failed to add partner ${partner.id}", e); - } - return false; - } - - Future updatePartner(UserDto partner, {required bool inTimeline}) async { - try { - final dto = await _partnerApiRepository.update(partner.id, inTimeline: inTimeline); - await _isarUserRepository.update(partner.copyWith(inTimeline: dto.inTimeline)); - return true; - } catch (e) { - _log.warning("Failed to update partner ${partner.id}", e); - } - return false; - } -} diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index 37b16a8d29..023c62ed78 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,8 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,19 +7,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'person.service.g.dart'; @riverpod -PersonService personService(Ref ref) => PersonService( - ref.watch(personApiRepositoryProvider), - ref.watch(assetApiRepositoryProvider), - ref.read(assetRepositoryProvider), -); +PersonService personService(Ref ref) => PersonService(ref.watch(personApiRepositoryProvider)); class PersonService { final Logger _log = Logger("PersonService"); final PersonApiRepository _personApiRepository; - final AssetApiRepository _assetApiRepository; - final AssetRepository _assetRepository; - - PersonService(this._personApiRepository, this._assetApiRepository, this._assetRepository); + PersonService(this._personApiRepository); Future> getAllPeople() async { try { @@ -33,16 +23,6 @@ class PersonService { } } - Future> getPersonAssets(String id) async { - try { - final assets = await _assetApiRepository.search(personIds: [id]); - return await _assetRepository.getAllByRemoteId(assets.map((a) => a.remoteId!)); - } catch (error, stack) { - _log.severe("Error while fetching person assets", error, stack); - } - return []; - } - Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart index 8c2d46b3bd..4caf1ea434 100644 --- a/mobile/lib/services/person.service.g.dart +++ b/mobile/lib/services/person.service.g.dart @@ -6,7 +6,7 @@ part of 'person.service.dart'; // RiverpodGenerator // ************************************************************************** -String _$personServiceHash() => r'10883bccc6c402205e6785cf9ee6cd7142cd0983'; +String _$personServiceHash() => r'646e38d764c52e63d9fca86992e440f34196d519'; /// See also [personService]. @ProviderFor(personService) diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index f33adf80f9..0330c8485c 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,31 +1,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/models/search/search_result.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; final searchServiceProvider = Provider( - (ref) => SearchService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(searchApiRepositoryProvider), - ), + (ref) => SearchService(ref.watch(apiServiceProvider), ref.watch(searchApiRepositoryProvider)), ); class SearchService { final ApiService _apiService; - final AssetRepository _assetRepository; final SearchApiRepository _searchApiRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._assetRepository, this._searchApiRepository); + SearchService(this._apiService, this._searchApiRepository); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -48,24 +39,6 @@ class SearchService { } } - Future search(SearchFilter filter, int page) async { - try { - final response = await _searchApiRepository.search(filter, page); - - if (response == null || response.assets.items.isEmpty) { - return null; - } - - return SearchResult( - assets: await _assetRepository.getAllByRemoteId(response.assets.items.map((e) => e.id)), - nextPage: response.assets.nextPage?.toInt(), - ); - } catch (error, stackTrace) { - _log.severe("Failed to search for assets", error, stackTrace); - } - return null; - } - Future?> getExploreData() async { try { return await _apiService.searchApi.getExploreData(); diff --git a/mobile/lib/services/share.service.dart b/mobile/lib/services/share.service.dart deleted file mode 100644 index a0998d6d3d..0000000000 --- a/mobile/lib/services/share.service.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; - -final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider))); - -class ShareService { - final ApiService _apiService; - final Logger _log = Logger("ShareService"); - - ShareService(this._apiService); - - Future shareAsset(Asset asset, BuildContext context) async { - return await shareAssets([asset], context); - } - - Future shareAssets(List assets, BuildContext context) async { - try { - final downloadedXFiles = []; - - for (var asset in assets) { - if (asset.isLocal) { - // Prefer local assets to share - File? f = await asset.local!.originFile; - downloadedXFiles.add(XFile(f!.path)); - } else if (asset.isRemote) { - // Download remote asset otherwise - final tempDir = await getTemporaryDirectory(); - final fileName = asset.fileName; - final tempFile = await File('${tempDir.path}/$fileName').create(); - final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(asset.remoteId!); - - if (res.statusCode != 200) { - _log.severe("Asset download for ${asset.fileName} failed", res.toLoggerString()); - continue; - } - - tempFile.writeAsBytesSync(res.bodyBytes); - downloadedXFiles.add(XFile(tempFile.path)); - } - } - - if (downloadedXFiles.isEmpty) { - _log.warning("No asset can be retrieved for share"); - return false; - } - - if (downloadedXFiles.length != assets.length) { - _log.warning("Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}"); - } - - final size = MediaQuery.of(context).size; - unawaited( - Share.shareXFiles( - downloadedXFiles, - sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), - ), - ); - return true; - } catch (error) { - _log.severe("Share failed", error); - } - return false; - } -} diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart deleted file mode 100644 index 88189c6bcd..0000000000 --- a/mobile/lib/services/stack.service.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class StackService { - const StackService(this._api, this._assetRepository); - - final ApiService _api; - final AssetRepository _assetRepository; - - Future getStack(String stackId) async { - try { - return _api.stacksApi.getStack(stackId); - } catch (error) { - dPrint(() => "Error while fetching stack: $error"); - } - return null; - } - - Future createStack(List assetIds) async { - try { - return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); - } catch (error) { - dPrint(() => "Error while creating stack: $error"); - } - return null; - } - - Future updateStack(String stackId, String primaryAssetId) async { - try { - return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); - } catch (error) { - dPrint(() => "Error while updating stack children: $error"); - } - return null; - } - - Future deleteStack(String stackId, List assets) async { - try { - await _api.stacksApi.deleteStack(stackId); - - // Update local database to trigger rerendering - final List removeAssets = []; - for (final asset in assets) { - asset.stackId = null; - asset.stackPrimaryAssetId = null; - asset.stackCount = 0; - - removeAssets.add(asset); - } - await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); - } catch (error) { - dPrint(() => "Error while deleting stack: $error"); - } - } -} - -final stackServiceProvider = Provider( - (ref) => StackService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)), -); diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart deleted file mode 100644 index f5b55f36eb..0000000000 --- a/mobile/lib/services/sync.service.dart +++ /dev/null @@ -1,945 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:immich_mobile/utils/datetime_comparison.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:logging/logging.dart'; - -final syncServiceProvider = Provider( - (ref) => SyncService( - ref.watch(hashServiceProvider), - ref.watch(entityServiceProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(albumApiRepositoryProvider), - ref.watch(albumRepositoryProvider), - ref.watch(assetRepositoryProvider), - ref.watch(exifRepositoryProvider), - ref.watch(partnerRepositoryProvider), - ref.watch(userRepositoryProvider), - ref.watch(userServiceProvider), - ref.watch(etagRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(localFilesManagerRepositoryProvider), - ref.watch(partnerApiRepositoryProvider), - ref.watch(userApiRepositoryProvider), - ), -); - -class SyncService { - final HashService _hashService; - final EntityService _entityService; - final AlbumMediaRepository _albumMediaRepository; - final AlbumApiRepository _albumApiRepository; - final AlbumRepository _albumRepository; - final AssetRepository _assetRepository; - final IsarExifRepository _exifInfoRepository; - final IsarUserRepository _isarUserRepository; - final UserService _userService; - final PartnerRepository _partnerRepository; - final ETagRepository _eTagRepository; - final PartnerApiRepository _partnerApiRepository; - final UserApiRepository _userApiRepository; - final AsyncMutex _lock = AsyncMutex(); - final Logger _log = Logger('SyncService'); - final AppSettingsService _appSettingsService; - final LocalFilesManagerRepository _localFilesManager; - - SyncService( - this._hashService, - this._entityService, - this._albumMediaRepository, - this._albumApiRepository, - this._albumRepository, - this._assetRepository, - this._exifInfoRepository, - this._partnerRepository, - this._isarUserRepository, - this._userService, - this._eTagRepository, - this._appSettingsService, - this._localFilesManager, - this._partnerApiRepository, - this._userApiRepository, - ); - - // public methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) => _lock.run(() => _syncUsersFromServer(users)); - - /// Syncs remote assets owned by the logged-in user to the DB - /// Returns `true` if there were any changes - Future syncRemoteAssetsToDb({ - required List users, - required Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - required FutureOr?> Function(UserDto user, DateTime until) loadAssets, - }) => _lock.run( - () async => - await _syncRemoteAssetChanges(users, getChangedAssets) ?? - await _syncRemoteAssetsFull(getUsersFromServer, loadAssets), - ); - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future syncRemoteAlbumsToDb(List remote) => _lock.run(() => _syncRemoteAlbumsToDb(remote)); - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) => - _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); - - /// returns all Asset IDs that are not contained in the existing list - List sharedAssetsToRemove(List deleteCandidates, List existing) { - if (deleteCandidates.isEmpty) { - return []; - } - deleteCandidates.sort(Asset.compareById); - existing.sort(Asset.compareById); - return _diffAssets(existing, deleteCandidates, compare: Asset.compareById).$3.map((e) => e.id).toList(); - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future syncNewAssetToDb(Asset newAsset) => _lock.run(() => _syncNewAssetToDb(newAsset)); - - Future removeAllLocalAlbumsAndAssets() => _lock.run(_removeAllLocalAlbumsAndAssets); - - // private methods: - - /// Syncs users from the server to the local database - /// Returns `true`if there were any changes - Future _syncUsersFromServer(List users) async { - users.sortBy((u) => u.id); - final dbUsers = await _isarUserRepository.getAll(sortBy: SortUserBy.id); - final List toDelete = []; - final List toUpsert = []; - final changes = diffSortedListsSync( - users, - dbUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - if ((a.updatedAt == null && b.updatedAt != null) || - (a.updatedAt != null && b.updatedAt == null) || - (a.updatedAt != null && b.updatedAt != null && !a.updatedAt!.isAtSameMomentAs(b.updatedAt!)) || - a.isPartnerSharedBy != b.isPartnerSharedBy || - a.isPartnerSharedWith != b.isPartnerSharedWith || - a.inTimeline != b.inTimeline) { - toUpsert.add(a); - return true; - } - return false; - }, - onlyFirst: (UserDto a) => toUpsert.add(a), - onlySecond: (UserDto b) => toDelete.add(b.id), - ); - if (changes) { - await _isarUserRepository.transaction(() async { - await _isarUserRepository.delete(toDelete); - await _isarUserRepository.updateAll(toUpsert); - }); - } - return changes; - } - - /// Syncs a new asset to the db. Returns `true` if successful - Future _syncNewAssetToDb(Asset a) async { - final Asset? inDb = await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum); - if (inDb != null) { - // unify local/remote assets by replacing the - // local-only asset in the DB with a local&remote asset - a = inDb.updatedCopy(a); - } - try { - await _assetRepository.update(a); - } catch (e) { - _log.severe("Failed to put new asset into db", e); - return false; - } - return true; - } - - /// Efficiently syncs assets via changes. Returns `null` when a full sync is required. - Future _syncRemoteAssetChanges( - List users, - Future<(List? toUpsert, List? toDelete)> Function(List users, DateTime since) - getChangedAssets, - ) async { - final currentUser = _userService.getMyUser(); - final DateTime? since = (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); - if (since == null) return null; - final DateTime now = DateTime.now(); - final (toUpsert, toDelete) = await getChangedAssets(users, since); - if (toUpsert == null || toDelete == null) { - await _clearUserAssetsETag(users); - return null; - } - try { - if (toDelete.isNotEmpty) { - await handleRemoteAssetRemoval(toDelete); - } - if (toUpsert.isNotEmpty) { - final (_, updated) = await _linkWithExistingFromDb(toUpsert); - await upsertAssetsWithExif(updated); - } - if (toUpsert.isNotEmpty || toDelete.isNotEmpty) { - await _updateUserAssetsETag(users, now); - return true; - } - return false; - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - return null; - } - - Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { - final List localAssets = await _assetRepository.getAllLocal(); - final List matchedAssets = localAssets.where((asset) => idsToDelete.contains(asset.remoteId)).toList(); - - final mediaUrls = await Future.wait(matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null))); - - await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); - } - - /// Deletes remote-only assets, updates merged assets to be local-only - Future handleRemoteAssetRemoval(List idsToDelete) async { - return _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(idsToDelete, state: AssetState.remote); - final merged = await _assetRepository.getAllByRemoteId(idsToDelete, state: AssetState.merged); - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _moveToTrashMatchedAssets(idsToDelete); - } - if (merged.isEmpty) return; - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - await _assetRepository.updateAll(merged); - }); - } - - Future> _getAllAccessibleUsers() async { - final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); - sharedWith.add(_userService.getMyUser()); - return sharedWith.toList(); - } - - /// Syncs assets by loading and comparing all assets from the server. - Future _syncRemoteAssetsFull( - FutureOr?> Function() refreshUsers, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final serverUsers = await refreshUsers(); - if (serverUsers == null) { - _log.warning("_syncRemoteAssetsFull aborted because user refresh failed"); - return false; - } - await _syncUsersFromServer(serverUsers); - final List users = await _getAllAccessibleUsers(); - bool changes = false; - for (UserDto u in users) { - changes |= await _syncRemoteAssetsForUser(u, loadAssets); - } - return changes; - } - - Future _syncRemoteAssetsForUser( - UserDto user, - FutureOr?> Function(UserDto user, DateTime until) loadAssets, - ) async { - final DateTime now = DateTime.now().toUtc(); - final List? remote = await loadAssets(user, now); - if (remote == null) { - return false; - } - final List inDb = await _assetRepository.getAll(ownerId: user.id, sortBy: AssetSort.checksum); - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - - remote.sort(Asset.compareByChecksum); - - // filter our duplicates that might be introduced by the chunked retrieval - remote.uniqueConsecutive(compare: Asset.compareByChecksum); - - final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true); - if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) { - await _updateUserAssetsETag([user], now); - return false; - } - final idsToDelete = toRemove.map((e) => e.id).toList(); - try { - await _assetRepository.deleteByIds(idsToDelete); - await upsertAssetsWithExif(toAdd + toUpdate); - } catch (e) { - _log.severe("Failed to sync remote assets to db", e); - } - await _updateUserAssetsETag([user], now); - return true; - } - - Future _updateUserAssetsETag(List users, DateTime time) { - final etags = users.map((u) => ETag(id: u.id, time: time)).toList(); - return _eTagRepository.upsertAll(etags); - } - - Future _clearUserAssetsETag(List users) { - final ids = users.map((u) => u.id).toList(); - return _eTagRepository.deleteByIds(ids); - } - - /// Syncs remote albums to the database - /// returns `true` if there were any changes - Future _syncRemoteAlbumsToDb(List remoteAlbums) async { - remoteAlbums.sortBy((e) => e.remoteId!); - - final List dbAlbums = await _albumRepository.getAll(remote: true, sortBy: AlbumSort.remoteId); - - final List toDelete = []; - final List existing = []; - - final bool changes = await diffSortedLists( - remoteAlbums, - dbAlbums, - compare: (remoteAlbum, dbAlbum) => remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!), - both: (remoteAlbum, dbAlbum) => _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing), - onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing), - onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete), - ); - - if (toDelete.isNotEmpty) { - final List idsToRemove = sharedAssetsToRemove(toDelete, existing); - if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteByIds(idsToRemove); - } - } else { - assert(toDelete.isEmpty); - } - return changes; - } - - /// syncs albums from the server to the local database (does not support - /// syncing changes from local back to server) - /// accumulates - Future _syncRemoteAlbum(Album dto, Album album, List deleteCandidates, List existing) async { - if (!_hasRemoteAlbumChanged(dto, album)) { - return false; - } - // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp, - // i.e. it will always be null. Save it here. - final originalDto = dto; - dto = await _albumApiRepository.get(dto.remoteId!); - - final assetsInDb = await _assetRepository.getByAlbum(album, sortBy: AssetSort.ownerIdChecksum); - assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!"); - final List assetsOnRemote = dto.remoteAssets.toList(); - assetsOnRemote.sort(Asset.compareByOwnerChecksum); - final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb, compare: Asset.compareByOwnerChecksum); - - // update shared users - final List sharedUsers = album.sharedUsers.map((u) => u.toDto()).toList(growable: false); - sharedUsers.sort((a, b) => a.id.compareTo(b.id)); - final List users = dto.remoteUsers.map((u) => u.toDto()).toList()..sort((a, b) => a.id.compareTo(b.id)); - final List userIdsToAdd = []; - final List usersToUnlink = []; - diffSortedListsSync( - users, - sharedUsers, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (a, b) => false, - onlyFirst: (UserDto a) => userIdsToAdd.add(a.id), - onlySecond: (UserDto a) => usersToUnlink.add(a), - ); - - // for shared album: put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - await upsertAssetsWithExif(updated); - final assetsToLink = existingInDb + updated; - final usersToLink = await _isarUserRepository.getByUserIds(userIdsToAdd); - - album.name = dto.name; - album.description = dto.description; - album.shared = dto.shared; - album.createdAt = dto.createdAt; - album.modifiedAt = dto.modifiedAt; - album.startDate = dto.startDate; - album.endDate = dto.endDate; - album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; - album.shared = dto.shared; - album.activityEnabled = dto.activityEnabled; - album.sortOrder = dto.sortOrder; - - final remoteThumbnailAssetId = dto.remoteThumbnailAssetId; - if (remoteThumbnailAssetId != null && album.thumbnail.value?.remoteId != remoteThumbnailAssetId) { - album.thumbnail.value = await _assetRepository.getByRemoteId(remoteThumbnailAssetId); - } - - // write & commit all changes to DB - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(toUpdate); - await _albumRepository.addUsers(album, usersToLink.nonNulls.toList()); - await _albumRepository.removeUsers(album, usersToUnlink); - await _albumRepository.addAssets(album, assetsToLink); - await _albumRepository.removeAssets(album, toUnlink); - await _albumRepository.recalculateMetadata(album); - await _albumRepository.update(album); - }); - _log.info("Synced changes of remote album ${album.name} to DB"); - } catch (e) { - _log.severe("Failed to sync remote album to database", e); - } - - if (album.shared || dto.shared) { - final userId = (_userService.getMyUser()).id; - final foreign = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); - existing.addAll(foreign); - - // delete assets in DB unless they belong to this user or part of some other shared album - final isarUserId = fastHash(userId); - deleteCandidates.addAll(toUnlink.where((a) => a.ownerId != isarUserId)); - } - - return true; - } - - /// Adds a remote album to the database while making sure to add any foreign - /// (shared) assets to the database beforehand - /// accumulates assets already existing in the database - Future _addAlbumFromServer(Album album, List existing) async { - if (album.remoteAssetCount != album.remoteAssets.length) { - album = await _albumApiRepository.get(album.remoteId!); - } - if (album.remoteAssetCount == album.remoteAssets.length) { - // in case an album contains assets not yet present in local DB: - // put missing album assets into local DB - final (existingInDb, updated) = await _linkWithExistingFromDb(album.remoteAssets.toList()); - existing.addAll(existingInDb); - await upsertAssetsWithExif(updated); - - await _entityService.fillAlbumWithDatabaseEntities(album); - await _albumRepository.create(album); - } else { - _log.warning( - "Failed to add album from server: assetCount ${album.remoteAssetCount} != " - "asset array length ${album.remoteAssets.length} for album ${album.name}", - ); - } - } - - /// Accumulates all suitable album assets to the `deleteCandidates` and - /// removes the album from the database. - Future _removeAlbumFromDb(Album album, List deleteCandidates) async { - if (album.isLocal) { - _log.info("Removing local album $album from DB"); - // delete assets in DB unless they are remote or part of some other album - deleteCandidates.addAll(await _assetRepository.getByAlbum(album, state: AssetState.local)); - } else if (album.shared) { - // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner - final userIds = (await _getAllAccessibleUsers()).map((user) => user.id); - final orphanedAssets = await _assetRepository.getByAlbum(album, notOwnedBy: userIds); - deleteCandidates.addAll(orphanedAssets); - } - try { - await _albumRepository.delete(album.id); - _log.info("Removed local album $album from DB"); - } catch (e) { - _log.severe("Failed to remove local album $album from DB", e); - } - } - - /// Syncs all device albums and their assets to the database - /// Returns `true` if there were any changes - Future _syncLocalAlbumAssetsToDb(List onDevice, [Set? excludedAssets]) async { - onDevice.sort((a, b) => a.localId!.compareTo(b.localId!)); - final inDb = await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId); - final List deleteCandidates = []; - final List existing = []; - final bool anyChanges = await diffSortedLists( - onDevice, - inDb, - compare: (Album a, Album b) => a.localId!.compareTo(b.localId!), - both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(a, b, deleteCandidates, existing, excludedAssets), - onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets), - onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), - ); - _log.fine("Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete"); - final (toDelete, toUpdate) = _handleAssetRemoval(deleteCandidates, existing, remote: false); - _log.fine("${toDelete.length} assets to delete, ${toUpdate.length} to update"); - if (toDelete.isNotEmpty || toUpdate.isNotEmpty) { - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - }); - _log.info("Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB"); - } - return anyChanges; - } - - /// Syncs the device album to the album in the database - /// returns `true` if there were any changes - /// Accumulates asset candidates to delete and those already existing in DB - Future _syncAlbumInDbAndOnDevice( - Album deviceAlbum, - Album dbAlbum, - List deleteCandidates, - List existing, [ - Set? excludedAssets, - bool forceRefresh = false, - ]) async { - _log.info("Syncing a local album to DB: ${deviceAlbum.name}"); - if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - _log.info("Local album ${deviceAlbum.name} has changed. Syncing..."); - if (!forceRefresh && excludedAssets == null && await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - return true; - } - // general case, e.g. some assets have been deleted or there are excluded albums on iOS - final inDb = await _assetRepository.getByAlbum( - dbAlbum, - ownerId: (_userService.getMyUser()).id, - sortBy: AssetSort.checksum, - ); - - assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - final int assetCountOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final List onDevice = await _getHashedAssets(deviceAlbum, excludedAssets: excludedAssets); - _removeDuplicates(onDevice); - // _removeDuplicates sorts `onDevice` by checksum - final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); - if (toAdd.isEmpty && - toUpdate.isEmpty && - toDelete.isEmpty && - dbAlbum.name == deviceAlbum.name && - dbAlbum.description == deviceAlbum.description && - dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) { - // changes only affeted excluded albums - _log.info("Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync."); - if (assetCountOnDevice != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount) { - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - } - return false; - } - _log.info( - "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete", - ); - final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); - _log.info( - "Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update", - ); - deleteCandidates.addAll(toDelete); - existing.addAll(existingInDb); - dbAlbum.name = deviceAlbum.name; - dbAlbum.description = deviceAlbum.description; - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - if (dbAlbum.thumbnail.value != null && toDelete.contains(dbAlbum.thumbnail.value)) { - dbAlbum.thumbnail.value = null; - } - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated + toUpdate); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.removeAssets(dbAlbum, toDelete); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]); - }); - _log.info("Synced changes of local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e); - } - - return true; - } - - /// fast path for common case: only new assets were added to device album - /// returns `true` if successful, else `false` - Future _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async { - if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) { - _log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync."); - return false; - } - final int totalOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); - final int lastKnownTotal = (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? 0; - if (totalOnDevice <= lastKnownTotal) { - _log.info("Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync."); - return false; - } - final List newAssets = await _getHashedAssets( - deviceAlbum, - modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)), - modifiedUntil: deviceAlbum.modifiedAt, - ); - - if (totalOnDevice != lastKnownTotal + newAssets.length) { - _log.info( - "Local album ${deviceAlbum.name} totalOnDevice is not equal to lastKnownTotal + newAssets.length. Skipping sync.", - ); - return false; - } - dbAlbum.modifiedAt = deviceAlbum.modifiedAt; - _removeDuplicates(newAssets); - final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets); - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(updated); - await _albumRepository.addAssets(dbAlbum, existingInDb + updated); - await _albumRepository.recalculateMetadata(dbAlbum); - await _albumRepository.update(dbAlbum); - await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)]); - }); - _log.info("Fast synced local album ${deviceAlbum.name} to DB"); - } catch (e) { - _log.severe("Failed to fast sync local album ${deviceAlbum.name} to DB", e); - return false; - } - - return true; - } - - /// Adds a new album from the device to the database and Accumulates all - /// assets already existing in the database to the list of `existing` assets - Future _addAlbumFromDevice(Album album, List existing, [Set? excludedAssets]) async { - _log.info("Adding a new local album to DB: ${album.name}"); - final assets = await _getHashedAssets(album, excludedAssets: excludedAssets); - _removeDuplicates(assets); - final (existingInDb, updated) = await _linkWithExistingFromDb(assets); - _log.info("${existingInDb.length} assets already existed in DB, to upsert ${updated.length}"); - await upsertAssetsWithExif(updated); - existing.addAll(existingInDb); - album.assets.addAll(existingInDb); - album.assets.addAll(updated); - final thumb = existingInDb.firstOrNull ?? updated.firstOrNull; - album.thumbnail.value = thumb; - try { - await _albumRepository.create(album); - final int assetCount = await _albumMediaRepository.getAssetCount(album.localId!); - await _eTagRepository.upsertAll([ETag(id: album.eTagKeyAssetCount, assetCount: assetCount)]); - _log.info("Added a new local album to DB: ${album.name}"); - } catch (e) { - _log.severe("Failed to add new local album ${album.name} to DB", e); - } - } - - /// Returns a tuple (existing, updated) - Future<(List existing, List updated)> _linkWithExistingFromDb(List assets) async { - if (assets.isEmpty) return ([].cast(), [].cast()); - - final List inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((a) => a.ownerId).toInt64List(), - assets.map((a) => a.checksum).toList(growable: false), - ); - assert(inDb.length == assets.length); - final List existing = [], toUpsert = []; - for (int i = 0; i < assets.length; i++) { - final Asset? b = inDb[i]; - if (b == null) { - toUpsert.add(assets[i]); - continue; - } - if (b.canUpdate(assets[i])) { - final updated = b.updatedCopy(assets[i]); - assert(updated.isInDb); - toUpsert.add(updated); - } else { - existing.add(b); - } - } - assert(existing.length + toUpsert.length == assets.length); - return (existing, toUpsert); - } - - Future _toggleTrashStatusForAssets(List assetsList) async { - final trashMediaUrls = []; - - for (final asset in assetsList) { - if (asset.isTrashed) { - final mediaUrl = await asset.local?.getMediaUrl(); - if (mediaUrl == null) { - _log.warning("Failed to get media URL for asset ${asset.name} while moving to trash"); - continue; - } - trashMediaUrls.add(mediaUrl); - } else { - await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index); - } - } - - if (trashMediaUrls.isNotEmpty) { - await _localFilesManager.moveToTrash(trashMediaUrls); - } - } - - /// Inserts or updates the assets in the database with their ExifInfo (if any) - Future upsertAssetsWithExif(List assets) async { - if (assets.isEmpty) return; - - if (Platform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) { - await _toggleTrashStatusForAssets(assets); - } - - try { - await _assetRepository.transaction(() async { - await _assetRepository.updateAll(assets); - for (final Asset added in assets) { - added.exifInfo = added.exifInfo?.copyWith(assetId: added.id); - } - final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList(); - await _exifInfoRepository.updateAll(exifInfos); - }); - _log.info("Upserted ${assets.length} assets into the DB"); - } catch (e) { - _log.severe("Failed to upsert ${assets.length} assets into the DB", e); - // give details on the errors - assets.sort(Asset.compareByOwnerChecksum); - final inDb = await _assetRepository.getAllByOwnerIdChecksum( - assets.map((e) => e.ownerId).toInt64List(), - assets.map((e) => e.checksum).toList(growable: false), - ); - for (int i = 0; i < assets.length; i++) { - final Asset a = assets[i]; - final Asset? b = inDb[i]; - if (b == null) { - if (!a.isInDb) { - _log.warning("Trying to update an asset that does not exist in DB:\n$a"); - } - } else if (a.id != b.id) { - _log.warning("Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a"); - } - } - for (int i = 1; i < assets.length; i++) { - if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) { - _log.warning("Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}"); - } - } - } - } - - /// Returns all assets that were successfully hashed - Future> _getHashedAssets( - Album album, { - int start = 0, - int end = 0x7fffffffffffffff, - DateTime? modifiedFrom, - DateTime? modifiedUntil, - Set? excludedAssets, - }) async { - final entities = await _albumMediaRepository.getAssets( - album.localId!, - start: start, - end: end, - modifiedFrom: modifiedFrom, - modifiedUntil: modifiedUntil, - ); - final filtered = excludedAssets == null - ? entities - : entities.where((e) => !excludedAssets.contains(e.localId!)).toList(); - return _hashService.hashAssets(filtered); - } - - List _removeDuplicates(List assets) { - final int before = assets.length; - assets.sort(Asset.compareByOwnerChecksumCreatedModified); - assets.uniqueConsecutive(compare: Asset.compareByOwnerChecksum, onDuplicate: (a, b) => {}); - final int duplicates = before - assets.length; - if (duplicates > 0) { - _log.warning("Ignored $duplicates duplicate assets on device"); - } - return assets; - } - - /// returns `true` if the albums differ on the surface - Future _hasAlbumChangeOnDevice(Album deviceAlbum, Album dbAlbum) async { - return deviceAlbum.name != dbAlbum.name || - deviceAlbum.description != dbAlbum.description || - !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != - (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount; - } - - Future _removeAllLocalAlbumsAndAssets() async { - try { - final assets = await _assetRepository.getAllLocal(); - final (toDelete, toUpdate) = _handleAssetRemoval(assets, [], remote: false); - await _assetRepository.transaction(() async { - await _assetRepository.deleteByIds(toDelete); - await _assetRepository.updateAll(toUpdate); - await _albumRepository.deleteAllLocal(); - }); - return true; - } catch (e) { - _log.severe("Failed to remove all local albums and assets", e); - return false; - } - } - - Future?> getUsersFromServer() async { - List? users; - try { - users = await _userApiRepository.getAll(); - } catch (e) { - _log.warning("Failed to fetch users", e); - users = null; - } - final List sharedBy = await _partnerApiRepository.getAll(Direction.sharedByMe); - final List sharedWith = await _partnerApiRepository.getAll(Direction.sharedWithMe); - - if (users == null) { - _log.warning("Failed to refresh users"); - return null; - } - - users.sortBy((u) => u.id); - sharedBy.sortBy((u) => u.id); - sharedWith.sortBy((u) => u.id); - - final updatedSharedBy = []; - - diffSortedListsSync( - users, - sharedBy, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedBy.add(a), - onlySecond: (UserDto b) => updatedSharedBy.add(b), - ); - - final updatedSharedWith = []; - - diffSortedListsSync( - updatedSharedBy, - sharedWith, - compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), - both: (UserDto a, UserDto b) { - updatedSharedWith.add(a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true)); - return true; - }, - onlyFirst: (UserDto a) => updatedSharedWith.add(a), - onlySecond: (UserDto b) => updatedSharedWith.add(b), - ); - - return updatedSharedWith; - } -} - -/// Returns a triple(toAdd, toUpdate, toRemove) -(List toAdd, List toUpdate, List toRemove) _diffAssets( - List assets, - List inDb, { - bool? remote, - int Function(Asset, Asset) compare = Asset.compareByChecksum, -}) { - // fast paths for trivial cases: reduces memory usage during initial sync etc. - if (assets.isEmpty && inDb.isEmpty) { - return const ([], [], []); - } else if (assets.isEmpty && remote == null) { - // remove all from database - return (const [], const [], inDb); - } else if (inDb.isEmpty) { - // add all assets - return (assets, const [], const []); - } - - final List toAdd = []; - final List toUpdate = []; - final List toRemove = []; - diffSortedListsSync( - inDb, - assets, - compare: compare, - both: (Asset a, Asset b) { - if (a.canUpdate(b)) { - toUpdate.add(a.updatedCopy(b)); - return true; - } - return false; - }, - onlyFirst: (Asset a) { - if (remote == true && a.isLocal) { - if (a.remoteId != null) { - a.remoteId = null; - toUpdate.add(a); - } - } else if (remote == false && a.isRemote) { - if (a.isLocal) { - a.localId = null; - toUpdate.add(a); - } - } else { - toRemove.add(a); - } - }, - onlySecond: (Asset b) => toAdd.add(b), - ); - return (toAdd, toUpdate, toRemove); -} - -/// returns a tuple (toDelete toUpdate) when assets are to be deleted -(List toDelete, List toUpdate) _handleAssetRemoval( - List deleteCandidates, - List existing, { - bool? remote, -}) { - if (deleteCandidates.isEmpty) { - return const ([], []); - } - deleteCandidates.sort(Asset.compareById); - deleteCandidates.uniqueConsecutive(compare: Asset.compareById); - existing.sort(Asset.compareById); - existing.uniqueConsecutive(compare: Asset.compareById); - final (tooAdd, toUpdate, toRemove) = _diffAssets( - existing, - deleteCandidates, - compare: Asset.compareById, - remote: remote, - ); - assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval"); - return (toRemove.map((e) => e.id).toList(), toUpdate); -} - -/// returns `true` if the albums differ on the surface -bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) { - return remoteAlbum.remoteAssetCount != dbAlbum.assetCount || - remoteAlbum.name != dbAlbum.name || - remoteAlbum.description != dbAlbum.description || - remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId || - remoteAlbum.shared != dbAlbum.shared || - remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length || - !remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || - !isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) || - !isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) || - !isAtSameMomentAs(remoteAlbum.lastModifiedAssetTimestamp, dbAlbum.lastModifiedAssetTimestamp); -} diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart deleted file mode 100644 index eaff1027d8..0000000000 --- a/mobile/lib/services/timeline.service.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - -final timelineServiceProvider = Provider((ref) { - return TimelineService( - ref.watch(timelineRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(userServiceProvider), - ); -}); - -class TimelineService { - final TimelineRepository _timelineRepository; - final AppSettingsService _appSettingsService; - final UserService _userService; - - const TimelineService(this._timelineRepository, this._appSettingsService, this._userService); - - Future> getTimelineUserIds() async { - final me = _userService.getMyUser(); - return _timelineRepository.getTimelineUserIds(me.id); - } - - Stream> watchTimelineUserIds() async* { - final me = _userService.getMyUser(); - yield* _timelineRepository.watchTimelineUsers(me.id); - } - - Stream watchHomeTimeline(String userId) { - return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption()); - } - - Stream watchMultiUsersTimeline(List userIds) { - return _timelineRepository.watchMultiUsersTimeline(userIds, _getGroupByOption()); - } - - Stream watchArchiveTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchArchiveTimeline(user.id); - } - - Stream watchFavoriteTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchFavoriteTimeline(user.id); - } - - Stream watchAlbumTimeline(Album album) async* { - yield* _timelineRepository.watchAlbumTimeline(album, _getGroupByOption()); - } - - Stream watchTrashTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchTrashTimeline(user.id); - } - - Stream watchAllVideosTimeline() { - final user = _userService.getMyUser(); - - return _timelineRepository.watchAllVideosTimeline(user.id); - } - - Future getTimelineFromAssets(List assets, GroupAssetsBy? groupBy) { - GroupAssetsBy groupOption = GroupAssetsBy.none; - if (groupBy == null) { - groupOption = _getGroupByOption(); - } else { - groupOption = groupBy; - } - - return _timelineRepository.getTimelineFromAssets(assets, groupOption); - } - - Stream watchAssetSelectionTimeline() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchAssetSelectionTimeline(user.id); - } - - GroupAssetsBy _getGroupByOption() { - return GroupAssetsBy.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; - } - - Stream watchLockedTimelineProvider() async* { - final user = _userService.getMyUser(); - - yield* _timelineRepository.watchLockedTimeline(user.id, _getGroupByOption()); - } -} diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart deleted file mode 100644 index 2c51a68c59..0000000000 --- a/mobile/lib/services/trash.service.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; - -final trashServiceProvider = Provider((ref) { - return TrashService( - ref.watch(apiServiceProvider), - ref.watch(assetRepositoryProvider), - ref.watch(userServiceProvider), - ); -}); - -class TrashService { - final ApiService _apiService; - final AssetRepository _assetRepository; - final UserService _userService; - - const TrashService(this._apiService, this._assetRepository, this._userService); - - Future restoreAssets(Iterable assetList) async { - final remoteAssets = assetList.where((a) => a.isRemote); - await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList())); - - final updatedAssets = remoteAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } - - Future emptyTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.emptyTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final ids = trashedAssets.map((e) => e.remoteId!).toList(); - - await _assetRepository.transaction(() async { - await _assetRepository.deleteAllByRemoteId(ids, state: AssetState.remote); - - final merged = await _assetRepository.getAllByRemoteId(ids, state: AssetState.merged); - if (merged.isEmpty) { - return; - } - - for (final Asset asset in merged) { - asset.remoteId = null; - asset.isTrashed = false; - } - - await _assetRepository.updateAll(merged); - }); - } - - Future restoreTrash() async { - final user = _userService.getMyUser(); - - await _apiService.trashApi.restoreTrash(); - - final trashedAssets = await _assetRepository.getTrashAssets(user.id); - final updatedAssets = trashedAssets.map((asset) { - asset.isTrashed = false; - return asset; - }).toList(); - - await _assetRepository.updateAll(updatedAssets); - } -} diff --git a/mobile/lib/utils/backup_progress.dart b/mobile/lib/utils/backup_progress.dart deleted file mode 100644 index 36050f5e20..0000000000 --- a/mobile/lib/utils/backup_progress.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:easy_localization/easy_localization.dart'; - -final NumberFormat numberFormat = NumberFormat("###0.##"); - -String formatAssetBackupProgress(int uploadedAssets, int assetsToUpload) { - final int percent = (uploadedAssets * 100) ~/ assetsToUpload; - return "$percent% ($uploadedAssets/$assetsToUpload)"; -} - -/// prints progress in useful (kilo/mega/giga)bytes -String humanReadableFileBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; - - if (bytesTotal >= 0x40000000) { - unit = "GB"; - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "${(bytes).toStringAsFixed(2)} B / ${(bytesTotal).toStringAsFixed(2)} B"; - } - - return "${(bytes / 1024.0).toStringAsFixed(2)} $unit / ${(bytesTotal / 1024.0).toStringAsFixed(2)} $unit"; -} - -/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes -String humanReadableBytesProgress(int bytes, int bytesTotal) { - String unit = "KB"; // Kilobyte - if (bytesTotal >= 0x40000000) { - unit = "GB"; // Gigabyte - bytes >>= 20; - bytesTotal >>= 20; - } else if (bytesTotal >= 0x100000) { - unit = "MB"; // Megabyte - bytes >>= 10; - bytesTotal >>= 10; - } else if (bytesTotal < 0x400) { - return "$bytes / $bytesTotal B"; - } - final int percent = (bytes * 100) ~/ bytesTotal; - final String done = numberFormat.format(bytes / 1024.0); - final String total = numberFormat.format(bytesTotal / 1024.0); - return "$percent% ($done/$total$unit)"; -} - -class ThrottleProgressUpdate { - ThrottleProgressUpdate(this._fun, Duration interval) : _interval = interval.inMicroseconds; - final void Function(String?, int, int) _fun; - final int _interval; - int _invokedAt = 0; - Timer? _timer; - - String? title; - int progress = 0; - int total = 0; - - void call({final String? title, final int progress = 0, final int total = 0}) { - final time = Timeline.now; - this.title = title ?? this.title; - this.progress = progress; - this.total = total; - if (time > _invokedAt + _interval) { - _timer?.cancel(); - _onTimeElapsed(); - } else { - _timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed); - } - } - - void _onTimeElapsed() { - _invokedAt = Timeline.now; - _fun(title, progress, total); - _timer = null; - // clear title to not send/overwrite it next time if unchanged - title = null; - } -} diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index d63a92ba37..e79b06f53b 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,30 +1,14 @@ -import 'dart:io'; - import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; void configureFileDownloaderNotifications() { FileDownloader().configureNotificationForGroup( @@ -57,48 +41,10 @@ void configureFileDownloaderNotifications() { } abstract final class Bootstrap { - static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async { + static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async { final drift = Drift(); final logDb = DriftLogger(); - - Isar? isar = Isar.getInstance(); - - if (isar != null) { - return (isar, drift, logDb); - } - - final dir = await getApplicationDocumentsDirectory(); - isar = await Isar.open( - [ - StoreValueSchema, - AssetSchema, - AlbumSchema, - ExifInfoSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - if (Platform.isAndroid) AndroidDeviceAssetSchema, - if (Platform.isIOS) IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: dir.path, - maxSizeMiB: 2048, - inspector: kDebugMode, - ); - - return (isar, drift, logDb); - } - - static Future initDomain( - Isar db, - Drift drift, - DriftLogger logDb, { - bool listenStoreUpdates = true, - bool shouldBufferLogs = true, - }) async { - final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true; - final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); + final DriftStoreRepository storeRepo = DriftStoreRepository(drift); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); @@ -109,5 +55,8 @@ abstract final class Bootstrap { ); await NetworkRepository.init(); + // Remove once all asset operations are migrated to Native APIs + await PhotoManager.setIgnorePermissionCheck(true); + return (drift, logDb); } } diff --git a/mobile/lib/utils/color_filter_generator.dart b/mobile/lib/utils/color_filter_generator.dart deleted file mode 100644 index 92aed4b1a0..0000000000 --- a/mobile/lib/utils/color_filter_generator.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class InvertionFilter extends StatelessWidget { - final Widget? child; - const InvertionFilter({super.key, this.child}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - -1, 0, 0, 0, 255, // - 0, -1, 0, 0, 255, // - 0, 0, -1, 0, 255, // - 0, 0, 0, 1, 0, // - ]), - child: child, - ); - } -} - -// -1 - darkest, 1 - brightest, 0 - unchanged -class BrightnessFilter extends StatelessWidget { - final Widget? child; - final double brightness; - const BrightnessFilter({super.key, this.child, this.brightness = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.brightnessAdjustMatrix(brightness)), - child: child, - ); - } -} - -// -1 - greyscale, 1 - most saturated, 0 - unchanged -class SaturationFilter extends StatelessWidget { - final Widget? child; - final double saturation; - const SaturationFilter({super.key, this.child, this.saturation = 0}); - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_ColorFilterGenerator.saturationAdjustMatrix(saturation)), - child: child, - ); - } -} - -class _ColorFilterGenerator { - static List brightnessAdjustMatrix(double value) { - value = value * 10; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - return List.from([ - 1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } - - static List saturationAdjustMatrix(double value) { - value = value * 100; - - if (value == 0) { - return [ - 1, 0, 0, 0, 0, // - 0, 1, 0, 0, 0, // - 0, 0, 1, 0, 0, // - 0, 0, 0, 1, 0, // - ]; - } - - double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); - double lumR = 0.3086; - double lumG = 0.6094; - double lumB = 0.082; - - return List.from([ - (lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - (lumG * (1 - x)) + x, // - lumB * (1 - x), // - 0, 0, // - lumR * (1 - x), // - lumG * (1 - x), // - (lumB * (1 - x)) + x, // - 0, 0, 0, 0, 0, 1, 0, // - ]).map((i) => i.toDouble()).toList(); - } -} diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart deleted file mode 100644 index f8ddcfea11..0000000000 --- a/mobile/lib/utils/datetime_comparison.dart +++ /dev/null @@ -1,2 +0,0 @@ -bool isAtSameMomentAs(DateTime? a, DateTime? b) => - (a == null && b == null) || ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart new file mode 100644 index 0000000000..fa2dedf383 --- /dev/null +++ b/mobile/lib/utils/editor.utils.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/matrix.utils.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; + +Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, int originalHeight) { + return Rect.fromLTWH( + parameters.x.toDouble() / originalWidth, + parameters.y.toDouble() / originalHeight, + parameters.width.toDouble() / originalWidth, + parameters.height.toDouble() / originalHeight, + ); +} + +CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) { + final x = (rect.left * originalWidth).truncate(); + final y = (rect.top * originalHeight).truncate(); + final width = (rect.width * originalWidth).truncate(); + final height = (rect.height * originalHeight).truncate(); + + return CropParameters( + x: max(x, 0).clamp(0, originalWidth), + y: max(y, 0).clamp(0, originalHeight), + width: max(width, 0).clamp(0, originalWidth - x), + height: max(height, 0).clamp(0, originalHeight - y), + ); +} + +AffineMatrix buildAffineFromEdits(List edits) { + return AffineMatrix.compose( + edits.map((edit) { + return switch (edit) { + RotateEdit(:final parameters) => AffineMatrix.rotate(parameters.angle * pi / 180), + MirrorEdit(:final parameters) => + parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX(), + CropEdit() => AffineMatrix.identity(), + }; + }).toList(), + ); +} + +bool isCloseToZero(double value, [double epsilon = 1e-15]) { + return value.abs() < epsilon; +} + +typedef NormalizedTransform = ({double rotation, bool mirrorHorizontal, bool mirrorVertical}); + +NormalizedTransform normalizeTransformEdits(List edits) { + final matrix = buildAffineFromEdits(edits); + + double a = matrix.a; + double b = matrix.b; + double c = matrix.c; + double d = matrix.d; + + final rotation = ((isCloseToZero(a) ? asin(c) : acos(a)) * 180) / pi; + + return ( + rotation: rotation < 0 ? 360 + rotation : rotation, + mirrorHorizontal: false, + mirrorVertical: isCloseToZero(a) ? b == c : a == -d, + ); +} diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index ac5fd31724..534c0ad8fb 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -1,20 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; + import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:thumbhash/thumbhash.dart' as thumbhash; -ObjectRef useBlurHashRef(Asset? asset) { - if (asset?.thumbhash == null) { - return useRef(null); - } - - final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!)); - - return useRef(thumbhash.rgbaToBmp(rbga)); -} - ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { if (asset?.thumbHash == null) { return useRef(null); diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 079f0e51fa..c562049b1d 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,47 +1,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:openapi/api.dart'; -String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailUrlForRemoteId(asset.remoteId!, type: type); -} - -String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type); -} - -String getThumbnailCacheKeyForRemoteId( - final String id, - final String thumbhash, { - AssetMediaSize type = AssetMediaSize.thumbnail, -}) { - if (type == AssetMediaSize.thumbnail) { - return 'thumbnail-image-$id-$thumbhash'; - } else { - return '${id}_${thumbhash}_previewStage'; - } -} - -String getAlbumThumbnailUrl(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!, type: type); -} - -String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - if (album.thumbnail.value?.remoteId == null) { - return ''; - } - return getThumbnailCacheKeyForRemoteId( - album.thumbnail.value!.remoteId!, - album.thumbnail.value!.thumbhash!, - type: type, - ); -} - String getOriginalUrlForRemoteId(final String id, {bool edited = true}) { return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited'; } diff --git a/mobile/lib/utils/immich_loading_overlay.dart b/mobile/lib/utils/immich_loading_overlay.dart deleted file mode 100644 index be49c3bae9..0000000000 --- a/mobile/lib/utils/immich_loading_overlay.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -final _loadingEntry = OverlayEntry( - builder: (context) => SizedBox.square( - dimension: double.infinity, - child: DecoratedBox( - decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), - child: const Center( - child: DelayedLoadingIndicator(delay: Duration(seconds: 1), fadeInDuration: Duration(milliseconds: 400)), - ), - ), - ), -); - -ValueNotifier useProcessingOverlay() { - return use(const _LoadingOverlay()); -} - -class _LoadingOverlay extends Hook> { - const _LoadingOverlay(); - - @override - _LoadingOverlayState createState() => _LoadingOverlayState(); -} - -class _LoadingOverlayState extends HookState, _LoadingOverlay> { - late final _isLoading = ValueNotifier(false)..addListener(_listener); - OverlayEntry? _loadingOverlay; - - void _listener() { - setState(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isLoading.value) { - _loadingOverlay?.remove(); - _loadingOverlay = _loadingEntry; - Overlay.of(context).insert(_loadingEntry); - } else { - _loadingOverlay?.remove(); - _loadingOverlay = null; - } - }); - }); - } - - @override - ValueNotifier build(BuildContext context) { - return _isLoading; - } - - @override - void dispose() { - _isLoading.dispose(); - super.dispose(); - } - - @override - Object? get debugValue => _isLoading.value; - - @override - String get debugLabel => 'useProcessingOverlay<>'; -} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index c8224b9c55..20b56d4875 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -38,13 +37,9 @@ Cancelable runInIsolateGentle({ BackgroundIsolateBinaryMessenger.ensureInitialized(token); DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); final ref = ProviderContainer( overrides: [ - // TODO: Remove once isar is removed - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), cancellationProvider.overrideWithValue(cancelledChecker), driftProvider.overrideWith(driftOverride(drift)), ], @@ -66,15 +61,6 @@ Cancelable runInIsolateGentle({ await LogService.I.dispose(); await logDb.close(); await drift.close(); - - // Close Isar safely - try { - if (isar.isOpen) { - await isar.close(); - } - } catch (e) { - dPrint(() => "Error closing Isar: $e"); - } } catch (error, stack) { dPrint(() => "Error closing resources in isolate: $error, $stack"); } finally { diff --git a/mobile/lib/utils/matrix.utils.dart b/mobile/lib/utils/matrix.utils.dart new file mode 100644 index 0000000000..8363a8b93d --- /dev/null +++ b/mobile/lib/utils/matrix.utils.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +class AffineMatrix { + final double a; + final double b; + final double c; + final double d; + final double e; + final double f; + + const AffineMatrix(this.a, this.b, this.c, this.d, this.e, this.f); + + @override + String toString() { + return 'AffineMatrix(a: $a, b: $b, c: $c, d: $d, e: $e, f: $f)'; + } + + factory AffineMatrix.identity() { + return const AffineMatrix(1, 0, 0, 1, 0, 0); + } + + AffineMatrix multiply(AffineMatrix other) { + return AffineMatrix( + a * other.a + c * other.b, + b * other.a + d * other.b, + a * other.c + c * other.d, + b * other.c + d * other.d, + a * other.e + c * other.f + e, + b * other.e + d * other.f + f, + ); + } + + factory AffineMatrix.compose([List transformations = const []]) { + return transformations.fold(AffineMatrix.identity(), (acc, matrix) => acc.multiply(matrix)); + } + + factory AffineMatrix.rotate(double angle) { + final cosAngle = cos(angle); + final sinAngle = sin(angle); + return AffineMatrix(cosAngle, -sinAngle, sinAngle, cosAngle, 0, 0); + } + + factory AffineMatrix.flipY() { + return const AffineMatrix(-1, 0, 0, 1, 0, 0); + } + + factory AffineMatrix.flipX() { + return const AffineMatrix(1, 0, 0, -1, 0, 0); + } +} diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 76916cee1e..9ac805af39 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,115 +1,14 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart' as isar_backup_album; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:immich_mobile/platform/network_api.g.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/datetime_helpers.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; -// ignore: import_rule_photo_manager -import 'package:photo_manager/photo_manager.dart'; const int targetVersion = 25; -Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { - final hasVersion = Store.tryGet(StoreKey.version) != null; +Future migrateDatabaseIfNeeded() async { final int version = Store.get(StoreKey.version, targetVersion); - if (version < 9) { - await Store.put(StoreKey.version, targetVersion); - final value = await db.storeValues.get(StoreKey.currentUser.id); - if (value != null) { - final id = value.intValue; - if (id != null) { - await db.writeTxn(() async { - final user = await db.users.get(id); - await db.storeValues.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id)); - }); - } - } - } - - if (version < 10) { - await Store.put(StoreKey.version, targetVersion); - await _migrateDeviceAsset(db); - } - - if (version < 13) { - await Store.put(StoreKey.photoManagerCustomFilter, true); - } - - // This means that the SQLite DB is just created and has no version - if (version < 14 || !hasVersion) { - await migrateStoreToSqlite(db, drift); - await Store.populateCache(); - } - - final syncStreamRepository = SyncStreamRepository(drift); - await handleBetaMigration(version, await _isNewInstallation(db, drift), syncStreamRepository); - - if (version < 17 && Store.isBetaTimelineEnabled) { - final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue); - if (delay >= 1000) { - await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt()); - } - } - - if (version < 18 && Store.isBetaTimelineEnabled) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } - - if (version < 19 && Store.isBetaTimelineEnabled) { - if (!await _populateLocalAssetTime(drift)) { - return; - } - } - - if (version < 20 && Store.isBetaTimelineEnabled) { - await _syncLocalAlbumIsIosSharedAlbum(drift); - } - - if (version < 21) { - final certData = SSLClientCertStoreVal.load(); - if (certData != null) { - await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? "")); - } - } - - if (version < 23 && Store.isBetaTimelineEnabled) { - await _populateLocalAssetPlaybackStyle(drift); - } - - if (version < 24 && Store.isBetaTimelineEnabled) { - await _applyLocalAssetOrientation(drift); - } if (version < 25) { final accessToken = Store.tryGet(StoreKey.accessToken); @@ -121,365 +20,6 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } - if (version < 22 && !Store.isBetaTimelineEnabled) { - await Store.put(StoreKey.needBetaMigration, true); - } - - if (targetVersion >= 12) { - await Store.put(StoreKey.version, targetVersion); - return; - } - - final shouldTruncate = version < 8 || version < targetVersion; - - if (shouldTruncate) { - await _migrateTo(db, targetVersion); - } -} - -Future handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async { - // Handle migration only for this version - // TODO: remove when old timeline is removed - final isBeta = Store.tryGet(StoreKey.betaTimeline); - final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); - if (version <= 15 && needBetaMigration == null) { - // For new installations, no migration needed - // For existing installations, only migrate if beta timeline is not enabled (null or false) - if (isNewInstallation || isBeta == true) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, true); - } - } - - if (version > 15) { - if (isBeta == null || isBeta) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await Store.put(StoreKey.needBetaMigration, false); - } - } - - if (version < 16) { - await syncStreamRepository.reset(); - await Store.put(StoreKey.shouldResetSync, true); - } -} - -Future _isNewInstallation(Isar db, Drift drift) async { - try { - final isarUserCount = await db.users.count(); - if (isarUserCount > 0) { - return false; - } - - final isarAssetCount = await db.assets.count(); - if (isarAssetCount > 0) { - return false; - } - - final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length); - if (driftStoreCount > 0) { - return false; - } - - final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length); - if (driftAssetCount > 0) { - return false; - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error checking if new installation: $error"); - return false; - } -} - -Future _migrateTo(Isar db, int version) async { - await Store.delete(StoreKey.assetETag); - await db.writeTxn(() async { - await db.assets.clear(); - await db.exifInfos.clear(); - await db.albums.clear(); - await db.eTags.clear(); - await db.users.clear(); - }); - await Store.put(StoreKey.version, version); -} - -Future _migrateDeviceAsset(Isar db) async { - final ids = Platform.isAndroid - ? (await db.androidDeviceAssets.where().findAll()) - .map((a) => _DeviceAsset(assetId: a.id.toString(), hash: a.hash)) - .toList() - : (await db.iOSDeviceAssets.where().findAll()).map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)).toList(); - - final PermissionState ps = await PhotoManager.requestPermissionExtend(); - if (!ps.hasAccess) { - dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration."); - return; - } - - List<_DeviceAsset> localAssets = []; - final List paths = await PhotoManager.getAssetPathList(onlyAll: true); - - if (paths.isEmpty) { - localAssets = (await db.assets.where().anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)).findAll()) - .map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt)) - .toList(); - } else { - final AssetPathEntity albumWithAll = paths.first; - final int assetCount = await albumWithAll.assetCountAsync; - - final List allDeviceAssets = await albumWithAll.getAssetListRange(start: 0, end: assetCount); - - localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); - } - - dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}"); - dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}"); - ids.sort((a, b) => a.assetId.compareTo(b.assetId)); - localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); - final List toAdd = []; - await diffSortedLists( - ids, - localAssets, - compare: (a, b) => a.assetId.compareTo(b.assetId), - both: (deviceAsset, asset) { - toAdd.add( - DeviceAssetEntity(assetId: deviceAsset.assetId, hash: deviceAsset.hash!, modifiedTime: asset.dateTime!), - ); - return false; - }, - onlyFirst: (deviceAsset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); - }, - onlySecond: (asset) { - dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); - }, - ); - - dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); - - await db.writeTxn(() async { - await db.deviceAssetEntitys.putAll(toAdd); - }); -} - -Future _populateLocalAssetTime(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) async { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion( - longitude: Value(asset.longitude), - latitude: Value(asset.latitude), - adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)), - updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), - ), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - return true; - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating asset time: $error"); - return false; - } -} - -Future _syncLocalAlbumIsIosSharedAlbum(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - final albums = await nativeApi.getAlbums(); - await db.batch((batch) { - for (final album in albums) { - batch.update( - db.localAlbumEntity, - LocalAlbumEntityCompanion(isIosSharedAlbum: Value(album.isCloud)), - where: (t) => t.id.equals(album.id), - ); - } - }); - dPrint(() => "[MIGRATION] Successfully updated isIosSharedAlbum for ${albums.length} albums"); - } catch (error) { - dPrint(() => "[MIGRATION] Error while syncing local album isIosSharedAlbum: $error"); - } -} - -Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { - try { - final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); - await drift.batch((batch) { - for (final deviceAsset in isarDeviceAssets) { - batch.update( - drift.localAssetEntity, - LocalAssetEntityCompanion(checksum: Value(base64.encode(deviceAsset.hash))), - where: (t) => t.id.equals(deviceAsset.assetId), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error"); - } -} - -Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { - try { - final isarBackupAlbums = await db.backupAlbums.where().findAll(); - // Recents is a virtual album on Android, and we don't have it with the new sync - // If recents is selected previously, select all albums during migration except the excluded ones - if (Platform.isAndroid) { - final recentAlbum = isarBackupAlbums.firstWhereOrNull((album) => album.id == 'isAll'); - if (recentAlbum != null) { - await drift.localAlbumEntity.update().write( - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.selected)), - ); - final excluded = isarBackupAlbums - .where((album) => album.selection == isar_backup_album.BackupSelection.exclude) - .map((album) => album.id) - .toList(); - await drift.batch((batch) async { - for (final id in excluded) { - batch.update( - drift.localAlbumEntity, - const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.excluded)), - where: (t) => t.id.equals(id), - ); - } - }); - return; - } - } - - await drift.batch((batch) { - for (final album in isarBackupAlbums) { - batch.update( - drift.localAlbumEntity, - LocalAlbumEntityCompanion( - backupSelection: Value(switch (album.selection) { - isar_backup_album.BackupSelection.none => BackupSelection.none, - isar_backup_album.BackupSelection.select => BackupSelection.selected, - isar_backup_album.BackupSelection.exclude => BackupSelection.excluded, - }), - ), - where: (t) => t.id.equals(album.id), - ); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error"); - } -} - -Future migrateStoreToSqlite(Isar db, Drift drift) async { - try { - final isarStoreValues = await db.storeValues.where().findAll(); - await drift.batch((batch) { - for (final storeValue in isarStoreValues) { - final companion = StoreEntityCompanion( - id: Value(storeValue.id), - stringValue: Value(storeValue.strValue), - intValue: Value(storeValue.intValue), - ); - batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion)); - } - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error"); - } -} - -Future migrateStoreToIsar(Isar db, Drift drift) async { - try { - final driftStoreValues = await drift.storeEntity - .select() - .map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue)) - .get(); - - await db.writeTxn(() async { - await db.storeValues.putAll(driftStoreValues); - }); - } catch (error) { - dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error"); - } -} - -Future _populateLocalAssetPlaybackStyle(Drift db) async { - try { - final nativeApi = NativeSyncApi(); - - final albums = await nativeApi.getAlbums(); - for (final album in albums) { - final assets = await nativeApi.getAssetsForAlbum(album.id); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.localAssetEntity, - LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - - if (Platform.isAndroid) { - final trashedAssetMap = await nativeApi.getTrashedAssets(); - for (final entry in trashedAssetMap.cast>().entries) { - final assets = entry.value.cast(); - await db.batch((batch) { - for (final asset in assets) { - batch.update( - db.trashedLocalAssetEntity, - TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), - where: (t) => t.id.equals(asset.id), - ); - } - }); - } - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets"); - } else { - dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local assets"); - } - } catch (error) { - dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error"); - } -} - -Future _applyLocalAssetOrientation(Drift db) { - final query = db.localAssetEntity.update() - ..where((filter) => (filter.orientation.equals(90) | (filter.orientation.equals(270)))); - return query.write( - LocalAssetEntityCompanion.custom( - width: db.localAssetEntity.height, - height: db.localAssetEntity.width, - orientation: const Variable(0), - ), - ); -} - -AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) { - PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown, - PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image, - PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video, - PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated, - PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto, - PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping, -}; - -class _DeviceAsset { - final String assetId; - final List? hash; - final DateTime? dateTime; - - const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); + await Store.put(StoreKey.version, targetVersion); + return; } diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 090889ff32..38c805a42e 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -5,13 +5,13 @@ dynamic upgradeDto(dynamic value, String targetType) { case 'UserPreferencesResponseDto': if (value is Map) { addDefault(value, 'download.includeEmbeddedVideos', false); - addDefault(value, 'folders', FoldersResponse().toJson()); - addDefault(value, 'memories', MemoriesResponse().toJson()); - addDefault(value, 'ratings', RatingsResponse().toJson()); - addDefault(value, 'people', PeopleResponse().toJson()); - addDefault(value, 'tags', TagsResponse().toJson()); - addDefault(value, 'sharedLinks', SharedLinksResponse().toJson()); - addDefault(value, 'cast', CastResponse().toJson()); + addDefault(value, 'folders', FoldersResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'memories', MemoriesResponse(enabled: true, duration: 5).toJson()); + addDefault(value, 'ratings', RatingsResponse(enabled: false).toJson()); + addDefault(value, 'people', PeopleResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'tags', TagsResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'sharedLinks', SharedLinksResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'cast', CastResponse(gCastEnabled: false).toJson()); addDefault(value, 'albums', {'defaultAssetOrder': 'desc'}); } break; diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index 6c2d6e0f11..9524433c05 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -2,21 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/person_api.repository.dart'; -import 'package:immich_mobile/repositories/timeline.repository.dart'; void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(userApiRepositoryProvider); ref.invalidate(activityApiRepositoryProvider); ref.invalidate(partnerApiRepositoryProvider); - ref.invalidate(albumApiRepositoryProvider); ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); - ref.invalidate(timelineRepositoryProvider); ref.invalidate(searchApiRepositoryProvider); // Drift diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart deleted file mode 100644 index f0d333e262..0000000000 --- a/mobile/lib/utils/selection_handlers.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/date_time_picker.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -void handleShareAssets(WidgetRef ref, BuildContext context, Iterable selection) { - showDialog( - context: context, - builder: (BuildContext buildContext) { - ref.watch(shareServiceProvider).shareAssets(selection.toList(), context).then((bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }); - return const ShareDialog(); - }, - barrierDismissible: false, - useRootNavigator: false, - ); -} - -Future handleArchiveAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldArchive, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldArchive ??= !selection.every((a) => a.isArchived); - await ref.read(assetProvider.notifier).toggleArchive(selection, shouldArchive); - final message = shouldArchive - ? 'moved_to_archive'.t(context: context, args: {'count': selection.length}) - : 'moved_to_library'.t(context: context, args: {'count': selection.length}); - if (context.mounted) { - ImmichToast.show(context: context, msg: message, gravity: toastGravity); - } - } -} - -Future handleFavoriteAssets( - WidgetRef ref, - BuildContext context, - List selection, { - bool? shouldFavorite, - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { - if (selection.isNotEmpty) { - shouldFavorite ??= !selection.every((a) => a.isFavorite); - await ref.watch(assetProvider.notifier).toggleFavorite(selection, shouldFavorite); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = shouldFavorite - ? 'Added ${selection.length} $assetOrAssets to favorites' - : 'Removed ${selection.length} $assetOrAssets from favorites'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: toastGravity); - } - } -} - -Future handleEditDateTime(WidgetRef ref, BuildContext context, List selection) async { - DateTime? initialDate; - String? timeZone; - Duration? offset; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset(); - initialDate = dt; - offset = oft; - timeZone = assetWithExif.exifInfo?.timeZone; - } - final dateTime = await showDateTimePicker( - context: context, - initialDateTime: initialDate, - initialTZ: timeZone, - initialTZOffset: offset, - ); - - if (dateTime == null) { - return; - } - - await ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime); -} - -Future handleEditLocation(WidgetRef ref, BuildContext context, List selection) async { - LatLng? initialLatLng; - if (selection.length == 1) { - final asset = selection.first; - final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); - if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) { - initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!); - } - } - - final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); - - if (location == null) { - return; - } - - await ref.read(assetServiceProvider).changeLocation(selection.toList(), location); -} - -Future handleSetAssetsVisibility( - WidgetRef ref, - BuildContext context, - AssetVisibilityEnum visibility, - List selection, -) async { - if (selection.isNotEmpty) { - await ref.watch(assetProvider.notifier).setLockedView(selection, visibility); - - final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; - final toastMessage = visibility == AssetVisibilityEnum.locked - ? 'Added ${selection.length} $assetOrAssets to locked folder' - : 'Removed ${selection.length} $assetOrAssets from locked folder'; - if (context.mounted) { - ImmichToast.show(context: context, msg: toastMessage, gravity: ToastGravity.BOTTOM); - } - } -} diff --git a/mobile/lib/utils/string_helper.dart b/mobile/lib/utils/string_helper.dart deleted file mode 100644 index 201d141531..0000000000 --- a/mobile/lib/utils/string_helper.dart +++ /dev/null @@ -1,7 +0,0 @@ -extension StringExtension on String { - String capitalizeFirstLetter() { - return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; - } -} - -String s(num count) => (count == 1 ? '' : 's'); diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart deleted file mode 100644 index 8b41d92318..0000000000 --- a/mobile/lib/utils/throttle.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; - -/// Throttles function calls with the [interval] provided. -/// Also make sures to call the last Action after the elapsed interval -class Throttler { - final Duration interval; - DateTime? _lastActionTime; - - Throttler({required this.interval}); - - T? run(T Function() action) { - if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) { - final response = action(); - _lastActionTime = DateTime.now(); - return response; - } - - return null; - } - - void dispose() { - _lastActionTime = null; - } -} - -/// Creates a [Throttler] that will be disposed automatically. If no [interval] is provided, a -/// default interval of 300ms is used to throttle the function calls -Throttler useThrottler({Duration interval = const Duration(milliseconds: 300), List? keys}) => - use(_ThrottleHook(interval: interval, keys: keys)); - -class _ThrottleHook extends Hook { - const _ThrottleHook({required this.interval, super.keys}); - - final Duration interval; - - @override - HookState> createState() => _ThrottlerHookState(); -} - -class _ThrottlerHookState extends HookState { - late final throttler = Throttler(interval: hook.interval); - - @override - Throttler build(_) => throttler; - - @override - void dispose() => throttler.dispose(); - - @override - String get debugLabel => 'useThrottler'; -} diff --git a/mobile/lib/utils/thumbnail_utils.dart b/mobile/lib/utils/thumbnail_utils.dart deleted file mode 100644 index 685dc2b1c2..0000000000 --- a/mobile/lib/utils/thumbnail_utils.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; - -String getAltText(ExifInfo? exifInfo, DateTime fileCreatedAt, AssetType type, List peopleNames) { - if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) { - return exifInfo.description!; - } - final (template, args) = getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames); - return template.t(args: args); -} - -(String, Map) getAltTextTemplate( - ExifInfo? exifInfo, - DateTime fileCreatedAt, - AssetType type, - List peopleNames, -) { - final isVideo = type == AssetType.video; - final hasLocation = exifInfo?.city != null && exifInfo?.country != null; - final date = DateFormat.yMMMMd().format(fileCreatedAt); - final args = { - "isVideo": isVideo.toString(), - "date": date, - "city": exifInfo?.city ?? "", - "country": exifInfo?.country ?? "", - "person1": peopleNames.elementAtOrNull(0) ?? "", - "person2": peopleNames.elementAtOrNull(1) ?? "", - "person3": peopleNames.elementAtOrNull(2) ?? "", - "additionalCount": (peopleNames.length - 3).toString(), - }; - final template = hasLocation - ? (switch (peopleNames.length) { - 0 => "image_alt_text_date_place", - 1 => "image_alt_text_date_place_1_person", - 2 => "image_alt_text_date_place_2_people", - 3 => "image_alt_text_date_place_3_people", - _ => "image_alt_text_date_place_4_or_more_people", - }) - : (switch (peopleNames.length) { - 0 => "image_alt_text_date", - 1 => "image_alt_text_date_1_person", - 2 => "image_alt_text_date_2_people", - 3 => "image_alt_text_date_3_people", - _ => "image_alt_text_date_4_or_more_people", - }); - return (template, args); -} diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart deleted file mode 100644 index d21cdfbc94..0000000000 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTextField extends HookConsumerWidget { - final bool isEnabled; - final String? likeId; - final Function(String) onSubmit; - - const ActivityTextField({required this.onSubmit, this.isEnabled = true, this.likeId, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentAlbumProvider)!; - final asset = ref.watch(currentAssetProvider); - final activityNotifier = ref.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); - final user = ref.watch(currentUserProvider); - final inputController = useTextEditingController(); - final inputFocusNode = useFocusNode(); - final liked = likeId != null; - - // Show keyboard immediately on activities open - useEffect(() { - inputFocusNode.requestFocus(); - return null; - }, []); - - // Pass text to callback and reset controller - void onEditingComplete() { - onSubmit(inputController.text); - inputController.clear(); - inputFocusNode.unfocus(); - } - - Future addLike() async { - await activityNotifier.addLike(); - } - - Future removeLike() async { - if (liked) { - await activityNotifier.removeActivity(likeId!); - } - } - - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: TextField( - controller: inputController, - enabled: isEnabled, - focusNode: inputFocusNode, - textInputAction: TextInputAction.send, - autofocus: false, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - prefixIcon: user != null - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30), - ) - : null, - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 10), - child: IconButton( - icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt), - onPressed: liked ? removeLike : addLike, - ), - ), - suffixIconColor: liked ? context.primaryColor : null, - hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), - hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), - ), - onEditingComplete: onEditingComplete, - onTapOutside: (_) => inputFocusNode.unfocus(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart deleted file mode 100644 index ac3b6c95a4..0000000000 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/datetime_extensions.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ActivityTile extends HookConsumerWidget { - final Activity activity; - final bool isBottomSheet; - - const ActivityTile(this.activity, {super.key, this.isBottomSheet = false}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - final isLike = activity.type == ActivityType.like; - // Asset thumbnail is displayed when we are accessing activities from the album page - // currentAssetProvider will not be set until we open the gallery viewer - final showAssetThumbnail = asset == null && activity.assetId != null && !isBottomSheet; - - onTap() async { - final activityService = ref.read(activityServiceProvider); - final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); - if (route != null) { - await context.pushRoute(route); - } - } - - return ListTile( - minVerticalPadding: 15, - leading: isLike - ? Container( - width: isBottomSheet ? 30 : 44, - alignment: Alignment.center, - child: Icon(Icons.thumb_up, color: context.primaryColor), - ) - : isBottomSheet - ? UserCircleAvatar(user: activity.user, size: 30) - : UserCircleAvatar(user: activity.user), - title: _ActivityTitle( - userName: activity.user.name, - createdAt: activity.createdAt.timeAgo(), - leftAlign: isBottomSheet ? false : (isLike || showAssetThumbnail), - ), - // No subtitle for like, so center title - titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, - trailing: showAssetThumbnail ? _ActivityAssetThumbnail(activity.assetId!, onTap) : null, - subtitle: !isLike ? Text(activity.comment!) : null, - ); - } -} - -class _ActivityTitle extends StatelessWidget { - final String userName; - final String createdAt; - final bool leftAlign; - - const _ActivityTitle({required this.userName, required this.createdAt, required this.leftAlign}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - final textStyle = context.textTheme.bodyMedium?.copyWith(color: textColor.withValues(alpha: 0.6)); - - return Row( - mainAxisAlignment: leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, - mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, - children: [ - Text(userName, style: textStyle, overflow: TextOverflow.ellipsis), - if (leftAlign) Text(" • ", style: textStyle), - Expanded( - child: Text( - createdAt, - style: textStyle, - overflow: TextOverflow.ellipsis, - textAlign: leftAlign ? TextAlign.left : TextAlign.right, - ), - ), - ], - ); - } -} - -class _ActivityAssetThumbnail extends StatelessWidget { - final String assetId; - final GestureTapCallback? onTap; - - const _ActivityAssetThumbnail(this.assetId, this.onTap); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 40, - height: 30, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - image: DecorationImage( - image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: ""), - fit: BoxFit.cover, - ), - ), - child: const SizedBox.shrink(), - ), - ); - } -} diff --git a/mobile/lib/widgets/activities/dismissible_activity.dart b/mobile/lib/widgets/activities/dismissible_activity.dart index 806181ecdc..c056f5ee35 100644 --- a/mobile/lib/widgets/activities/dismissible_activity.dart +++ b/mobile/lib/widgets/activities/dismissible_activity.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -/// Wraps an [ActivityTile] and makes it dismissible class DismissibleActivity extends StatelessWidget { final String activityId; final Widget body; diff --git a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart deleted file mode 100644 index d8f6a8885a..0000000000 --- a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AddToAlbumBottomSheet extends HookConsumerWidget { - /// The asset to add to an album - final List assets; - - const AddToAlbumBottomSheet({super.key, required this.assets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final albumService = ref.watch(albumServiceProvider); - - useEffect(() { - // Fetch album updates, e.g., cover image - ref.read(albumProvider.notifier).refreshRemoteAlbums(); - - return null; - }, []); - - void addToAlbum(Album album) async { - final result = await albumService.addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}), - ); - } - } - context.pop(); - } - - return Card( - elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), - ), - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - const Align(alignment: Alignment.center, child: CustomDraggingHandle()), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('add_to_album'.tr(), style: context.textTheme.displayMedium), - TextButton.icon( - icon: Icon(Icons.add, color: context.primaryColor), - label: Text('common_create_new_album'.tr(), style: TextStyle(color: context.primaryColor)), - onPressed: () { - context.pushRoute(CreateAlbumRoute(assets: assets)); - }, - ), - ], - ), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: albums.where((a) => a.shared).toList(), - onAddToAlbum: addToAlbum, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/album/add_to_album_sliverlist.dart b/mobile/lib/widgets/album/add_to_album_sliverlist.dart deleted file mode 100644 index defbd90388..0000000000 --- a/mobile/lib/widgets/album/add_to_album_sliverlist.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_listtile.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; - -class AddToAlbumSliverList extends HookConsumerWidget { - /// The asset to add to an album - final List albums; - final List sharedAlbums; - final void Function(Album) onAddToAlbum; - final bool enabled; - - const AddToAlbumSliverList({ - super.key, - required this.onAddToAlbum, - required this.albums, - required this.sharedAlbums, - this.enabled = true, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortMode = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse); - final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse); - - return SliverList( - delegate: SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), ( - context, - index, - ) { - // Build shared expander - if (index == 0 && sortedSharedAlbums.isNotEmpty) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: ExpansionTile( - title: Text('shared'.tr()), - tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), - leading: const Icon(Icons.group), - children: [ - ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: sortedSharedAlbums.length, - itemBuilder: (context, index) => AlbumThumbnailListTile( - album: sortedSharedAlbums[index], - onTap: enabled ? () => onAddToAlbum(sortedSharedAlbums[index]) : () {}, - ), - ), - ], - ), - ); - } - - // Build albums list - final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); - final album = sortedAlbums[offset]; - return AlbumThumbnailListTile(album: album, onTap: enabled ? () => onAddToAlbum(album) : () {}); - }), - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart deleted file mode 100644 index 6c56f5d843..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class AlbumThumbnailCard extends ConsumerWidget { - final Function()? onTap; - - /// Whether or not to show the owner of the album (or "Owned") - /// in the subtitle of the album - final bool showOwner; - final bool showTitle; - - const AlbumThumbnailCard({super.key, required this.album, this.onTap, this.showOwner = false, this.showTitle = true}); - - final Album album; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return LayoutBuilder( - builder: (context, constraints) { - var cardSize = constraints.maxWidth; - - buildEmptyThumbnail() { - return Container( - height: cardSize, - width: cardSize, - decoration: BoxDecoration(color: context.colorScheme.surfaceContainerHigh), - child: Center( - child: Icon(Icons.no_photography, size: cardSize * .15, color: context.colorScheme.primary), - ), - ); - } - - buildAlbumThumbnail() => ImmichThumbnail(asset: album.thumbnail.value, width: cardSize, height: cardSize); - - buildAlbumTextRow() { - // Add the owner name to the subtitle - String? owner; - if (showOwner) { - if (album.ownerId == ref.read(currentUserProvider)?.id) { - owner = 'owned'.tr(); - } else if (album.ownerName != null) { - owner = 'shared_by_user'.t(context: context, args: {'user': album.ownerName!}); - } - } - - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'items_count'.t(context: context, args: {'count': album.assetCount}), - ), - if (owner != null) const TextSpan(text: ' • '), - if (owner != null) TextSpan(text: owner), - ], - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), - overflow: TextOverflow.fade, - ); - } - - return GestureDetector( - onTap: onTap, - child: Flex( - direction: Axis.vertical, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: cardSize, - height: cardSize, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - ), - if (showTitle) ...[ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: cardSize, - child: Text( - album.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - buildAlbumTextRow(), - ], - ], - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart deleted file mode 100644 index 386084b034..0000000000 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; - -class AlbumThumbnailListTile extends StatelessWidget { - const AlbumThumbnailListTile({super.key, required this.album, this.onTap}); - - final Album album; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - var cardSize = 68.0; - - buildEmptyThumbnail() { - return Container( - decoration: BoxDecoration(color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200]), - child: SizedBox( - height: cardSize, - width: cardSize, - child: const Center(child: Icon(Icons.no_photography)), - ), - ); - } - - buildAlbumThumbnail() { - return SizedBox( - width: cardSize, - height: cardSize, - child: Thumbnail( - imageProvider: RemoteImageProvider(url: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail)), - ), - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: - onTap ?? - () { - context.pushRoute(AlbumViewerRoute(albumId: album.id)); - }, - child: Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - album.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'items_count'.t(context: context, args: {'count': album.assetCount}), - style: const TextStyle(fontSize: 12), - ), - if (album.shared) ...[ - const Text(' • ', style: TextStyle(fontSize: 12)), - Text('shared'.tr(), style: const TextStyle(fontSize: 12)), - ], - ], - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart deleted file mode 100644 index 0a7438b7ae..0000000000 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_title.provider.dart'; - -class AlbumTitleTextField extends ConsumerWidget { - const AlbumTitleTextField({ - super.key, - required this.isAlbumTitleEmpty, - required this.albumTitleTextFieldFocusNode, - required this.albumTitleController, - required this.isAlbumTitleTextFieldFocus, - }); - - final ValueNotifier isAlbumTitleEmpty; - final FocusNode albumTitleTextFieldFocusNode; - final TextEditingController albumTitleController; - final ValueNotifier isAlbumTitleTextFieldFocus; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextField( - onChanged: (v) { - if (v.isEmpty) { - isAlbumTitleEmpty.value = true; - } else { - isAlbumTitleEmpty.value = false; - } - - ref.watch(albumTitleProvider.notifier).setAlbumTitle(v); - }, - focusNode: albumTitleTextFieldFocusNode, - style: TextStyle(fontSize: 28, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold), - controller: albumTitleController, - onTap: () { - isAlbumTitleTextFieldFocus.value = true; - - if (albumTitleController.text == 'Untitled') { - albumTitleController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - suffixIcon: !isAlbumTitleEmpty.value && isAlbumTitleTextFieldFocus.value - ? IconButton( - onPressed: () { - albumTitleController.clear(); - isAlbumTitleEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - focusColor: Colors.grey[300], - fillColor: context.colorScheme.surfaceContainerHigh, - filled: isAlbumTitleTextFieldFocus.value, - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart deleted file mode 100644 index 4fd4b31013..0000000000 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidget { - const AlbumViewerAppbar({ - super.key, - required this.userId, - required this.titleFocusNode, - required this.descriptionFocusNode, - this.onAddPhotos, - this.onAddUsers, - required this.onActivities, - }); - - final String userId; - final FocusNode titleFocusNode; - final FocusNode descriptionFocusNode; - final void Function()? onAddPhotos; - final void Function()? onAddUsers; - final void Function() onActivities; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumState = useState(ref.read(currentAlbumProvider)); - final album = albumState.value; - ref.listen(currentAlbumProvider, (_, newAlbum) { - final oldAlbum = albumState.value; - if (oldAlbum != null && newAlbum != null && oldAlbum.id == newAlbum.id) { - return; - } - - albumState.value = newAlbum; - }); - - if (album == null) { - return const SizedBox(); - } - - final albumViewer = ref.watch(albumViewerProvider); - final newAlbumTitle = albumViewer.editTitleText; - final newAlbumDescription = albumViewer.editDescriptionText; - final isEditAlbum = albumViewer.isEditAlbum; - - final comments = album.shared ? ref.watch(activityStatisticsProvider(album.remoteId!)) : 0; - - deleteAlbum() async { - final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album); - - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - - if (!success) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_delete".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - Future onDeleteAlbumPressed() { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return AlertDialog( - title: const Text('delete_album').tr(), - content: const Text('album_viewer_appbar_delete_confirm').tr(), - actions: [ - TextButton( - onPressed: () => context.pop('Cancel'), - child: Text( - 'cancel', - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () { - context.pop('Confirm'); - deleteAlbum(); - }, - child: Text( - 'confirm', - style: TextStyle(fontWeight: FontWeight.bold, color: context.colorScheme.error), - ).tr(), - ), - ], - ); - }, - ); - } - - void onLeaveAlbumPressed() async { - bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album); - - if (isSuccess) { - unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); - } else { - context.pop(); - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_leave".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - buildBottomSheetActions() { - return [ - album.ownerId == userId - ? ListTile( - leading: const Icon(Icons.delete_forever_rounded), - title: const Text('delete_album', style: TextStyle(fontWeight: FontWeight.w500)).tr(), - onTap: onDeleteAlbumPressed, - ) - : ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text( - 'album_viewer_appbar_share_leave', - style: TextStyle(fontWeight: FontWeight.w500), - ).tr(), - onTap: onLeaveAlbumPressed, - ), - ]; - // } - } - - void onSortOrderToggled() async { - final updatedAlbum = await ref.read(albumProvider.notifier).toggleSortOrder(album); - - if (updatedAlbum == null) { - ImmichToast.show( - context: context, - msg: "error_change_sort_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - context.pop(); - } - - void buildBottomSheet() { - final ownerActions = [ - ListTile( - leading: const Icon(Icons.person_add_alt_rounded), - onTap: () { - context.pop(); - final onAddUsers = this.onAddUsers; - if (onAddUsers != null) { - onAddUsers(); - } - }, - title: const Text("album_viewer_page_share_add_users", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.swap_vert_rounded), - onTap: onSortOrderToggled, - title: const Text("change_display_order", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.link_rounded), - onTap: () { - context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId)); - context.pop(); - }, - title: const Text("control_bottom_app_bar_share_link", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ListTile( - leading: const Icon(Icons.settings_rounded), - onTap: () => context.navigateTo(const AlbumOptionsRoute()), - title: const Text("options", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - - final commonActions = [ - ListTile( - leading: const Icon(Icons.add_photo_alternate_outlined), - onTap: () { - context.pop(); - final onAddPhotos = this.onAddPhotos; - if (onAddPhotos != null) { - onAddPhotos(); - } - }, - title: const Text("add_photos", style: TextStyle(fontWeight: FontWeight.w500)).tr(), - ), - ]; - showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: ListView( - shrinkWrap: true, - children: [ - ...buildBottomSheetActions(), - if (onAddPhotos != null) ...commonActions, - if (onAddPhotos != null && userId == album.ownerId) ...ownerActions, - ], - ), - ), - ); - }, - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: onActivities, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.mode_comment_outlined), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), - ), - ), - ], - ), - ); - } - - buildLeadingButton() { - if (isEditAlbum) { - return IconButton( - onPressed: () async { - if (newAlbumTitle.isNotEmpty) { - bool isSuccess = await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(album, newAlbumTitle); - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_title".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - titleFocusNode.unfocus(); - } else if (newAlbumDescription.isNotEmpty) { - bool isSuccessDescription = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumDescription(album, newAlbumDescription); - if (!isSuccessDescription) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_description".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - descriptionFocusNode.unfocus(); - } else { - titleFocusNode.unfocus(); - descriptionFocusNode.unfocus(); - ref.read(albumViewerProvider.notifier).disableEditAlbum(); - } - }, - icon: const Icon(Icons.check_rounded), - splashRadius: 25, - ); - } else { - return IconButton( - onPressed: context.maybePop, - icon: const Icon(Icons.arrow_back_ios_rounded), - splashRadius: 25, - ); - } - } - - return AppBar( - elevation: 0, - backgroundColor: context.scaffoldBackgroundColor, - leading: buildLeadingButton(), - centerTitle: false, - actions: [ - if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(), - if (album.isRemote) ...[ - IconButton(splashRadius: 25, onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded)), - ], - ], - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_description.dart b/mobile/lib/widgets/album/album_viewer_editable_description.dart deleted file mode 100644 index decd268ff3..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_description.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableDescription extends HookConsumerWidget { - final String albumDescription; - final FocusNode descriptionFocusNode; - const AlbumViewerEditableDescription({super.key, required this.albumDescription, required this.descriptionFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final descriptionTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editDescriptionText.isNotEmpty - ? albumViewerState.editDescriptionText - : albumDescription, - ); - - void onFocusModeChange() { - if (!descriptionFocusNode.hasFocus && descriptionTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(""); - descriptionTextEditController.text = ""; - } - } - - useEffect(() { - descriptionFocusNode.addListener(onFocusModeChange); - return () { - descriptionFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(value); - } - }, - focusNode: descriptionFocusNode, - style: context.textTheme.bodyLarge, - maxLines: 3, - minLines: 1, - controller: descriptionTextEditController, - onTap: () { - context.focusScope.requestFocus(descriptionFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditDescriptionText(albumDescription); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (descriptionTextEditController.text == '') { - descriptionTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(8), - suffixIcon: descriptionFocusNode.hasFocus - ? IconButton( - onPressed: () { - descriptionTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: descriptionFocusNode.hasFocus, - hintText: 'add_a_description'.tr(), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart deleted file mode 100644 index c84e613017..0000000000 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; - -class AlbumViewerEditableTitle extends HookConsumerWidget { - final String albumName; - final FocusNode titleFocusNode; - const AlbumViewerEditableTitle({super.key, required this.albumName, required this.titleFocusNode}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumViewerState = ref.watch(albumViewerProvider); - - final titleTextEditController = useTextEditingController( - text: albumViewerState.isEditAlbum && albumViewerState.editTitleText.isNotEmpty - ? albumViewerState.editTitleText - : albumName, - ); - - void onFocusModeChange() { - if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { - ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled"); - titleTextEditController.text = "Untitled"; - } - } - - useEffect(() { - titleFocusNode.addListener(onFocusModeChange); - return () { - titleFocusNode.removeListener(onFocusModeChange); - }; - }, []); - - return Material( - color: Colors.transparent, - child: TextField( - onChanged: (value) { - if (value.isEmpty) { - } else { - ref.watch(albumViewerProvider.notifier).setEditTitleText(value); - } - }, - focusNode: titleFocusNode, - style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700), - controller: titleTextEditController, - onTap: () { - context.focusScope.requestFocus(titleFocusNode); - - ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName); - ref.watch(albumViewerProvider.notifier).enableEditAlbum(); - - if (titleTextEditController.text == 'Untitled') { - titleTextEditController.clear(); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), - suffixIcon: titleFocusNode.hasFocus - ? IconButton( - onPressed: () { - titleTextEditController.clear(); - }, - icon: Icon(Icons.cancel_rounded, color: context.primaryColor), - splashRadius: 10, - ) - : null, - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), - focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, - filled: titleFocusNode.hasFocus, - hintText: 'add_a_title'.tr(), - hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(fontSize: 28), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart b/mobile/lib/widgets/album/shared_album_thumbnail_image.dart deleted file mode 100644 index b21e86d145..0000000000 --- a/mobile/lib/widgets/album/shared_album_thumbnail_image.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class SharedAlbumThumbnailImage extends HookConsumerWidget { - final Asset asset; - - const SharedAlbumThumbnailImage({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: () { - // debugPrint("View ${asset.id}"); - }, - child: Stack(children: [ImmichThumbnail(asset: asset, width: 500, height: 500)]), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/asset_drag_region.dart b/mobile/lib/widgets/asset_grid/asset_drag_region.dart deleted file mode 100644 index 71e55acbd6..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_drag_region.dart +++ /dev/null @@ -1,207 +0,0 @@ -// Based on https://stackoverflow.com/a/52625182 - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class AssetDragRegion extends StatefulWidget { - final Widget child; - - final void Function(AssetIndex valueKey)? onStart; - final void Function(AssetIndex valueKey)? onAssetEnter; - final void Function()? onEnd; - final void Function()? onScrollStart; - final void Function(ScrollDirection direction)? onScroll; - - const AssetDragRegion({ - super.key, - required this.child, - this.onStart, - this.onAssetEnter, - this.onEnd, - this.onScrollStart, - this.onScroll, - }); - @override - State createState() => _AssetDragRegionState(); -} - -class _AssetDragRegionState extends State { - late AssetIndex? assetUnderPointer; - late AssetIndex? anchorAsset; - - // Scroll related state - static const double scrollOffset = 0.10; - double? topScrollOffset; - double? bottomScrollOffset; - Timer? scrollTimer; - late bool scrollNotified; - - @override - void initState() { - super.initState(); - assetUnderPointer = null; - anchorAsset = null; - scrollNotified = false; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - topScrollOffset = null; - bottomScrollOffset = null; - } - - @override - void dispose() { - scrollTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - gestures: { - _CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>( - () => _CustomLongPressGestureRecognizer(), - _registerCallbacks, - ), - }, - child: widget.child, - ); - } - - void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) { - recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details); - recognizer.onLongPressStart = (details) => _onLongPressStart(details); - recognizer.onLongPressUp = _onLongPressEnd; - } - - AssetIndex? _getValueKeyAtPositon(Offset position) { - final box = context.findAncestorRenderObjectOfType(); - if (box == null) return null; - - final hitTestResult = BoxHitTestResult(); - final local = box.globalToLocal(position); - if (!box.hitTest(hitTestResult, position: local)) return null; - - return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)?.target as _AssetIndexProxy?) - ?.index; - } - - void _onLongPressStart(LongPressStartDetails event) { - /// Calculate widget height and scroll offset when long press starting instead of in [initState] - /// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size - final height = context.size?.height; - if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) { - topScrollOffset = height * scrollOffset; - bottomScrollOffset = height - topScrollOffset!; - } - - final initialHit = _getValueKeyAtPositon(event.globalPosition); - anchorAsset = initialHit; - if (initialHit == null) return; - - if (anchorAsset != null) { - widget.onStart?.call(anchorAsset!); - } - } - - void _onLongPressEnd() { - scrollNotified = false; - scrollTimer?.cancel(); - widget.onEnd?.call(); - } - - void _onLongPressMove(LongPressMoveUpdateDetails event) { - if (anchorAsset == null) return; - if (topScrollOffset == null || bottomScrollOffset == null) return; - - final currentDy = event.localPosition.dy; - - if (currentDy > bottomScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.forward), - ); - } else if (currentDy < topScrollOffset!) { - scrollTimer ??= Timer.periodic( - const Duration(milliseconds: 50), - (_) => widget.onScroll?.call(ScrollDirection.reverse), - ); - } else { - scrollTimer?.cancel(); - scrollTimer = null; - } - - final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition); - if (currentlyTouchingAsset == null) return; - - if (assetUnderPointer != currentlyTouchingAsset) { - if (!scrollNotified) { - scrollNotified = true; - widget.onScrollStart?.call(); - } - - widget.onAssetEnter?.call(currentlyTouchingAsset); - assetUnderPointer = currentlyTouchingAsset; - } - } -} - -class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} - -class AssetIndexWrapper extends SingleChildRenderObjectWidget { - final int rowIndex; - final int sectionIndex; - - const AssetIndexWrapper({required Widget super.child, required this.rowIndex, required this.sectionIndex, super.key}); - - @override - // ignore: library_private_types_in_public_api - _AssetIndexProxy createRenderObject(BuildContext context) { - return _AssetIndexProxy( - index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex), - ); - } - - @override - void updateRenderObject( - BuildContext context, - // ignore: library_private_types_in_public_api - _AssetIndexProxy renderObject, - ) { - renderObject.index = AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex); - } -} - -class _AssetIndexProxy extends RenderProxyBox { - AssetIndex index; - - _AssetIndexProxy({required this.index}); -} - -class AssetIndex { - final int rowIndex; - final int sectionIndex; - - const AssetIndex({required this.rowIndex, required this.sectionIndex}); - - @override - bool operator ==(covariant AssetIndex other) { - if (identical(this, other)) return true; - - return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex; - } - - @override - int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode; -} diff --git a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart b/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart deleted file mode 100644 index d95d6efe2e..0000000000 --- a/mobile/lib/widgets/asset_grid/asset_grid_data_structure.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; - -final log = Logger('AssetGridDataStructure'); - -enum RenderAssetGridElementType { assets, assetRow, groupDividerTitle, monthTitle } - -class RenderAssetGridElement { - final RenderAssetGridElementType type; - final String? title; - final DateTime date; - final int count; - final int offset; - final int totalCount; - - const RenderAssetGridElement( - this.type, { - this.title, - required this.date, - this.count = 0, - this.offset = 0, - this.totalCount = 0, - }); -} - -enum GroupAssetsBy { day, month, auto, none } - -class RenderList { - final List elements; - final List? allAssets; - final QueryBuilder? query; - final int totalAssets; - - /// reference to batch of assets loaded from DB with offset [_bufOffset] - List _buf = []; - - /// global offset of assets in [_buf] - int _bufOffset = 0; - - RenderList(this.elements, this.query, this.allAssets) : totalAssets = allAssets?.length ?? query!.countSync(); - - bool get isEmpty => totalAssets == 0; - - /// Loads the requested assets from the database to an internal buffer if not cached - /// and returns a slice of that buffer - List loadAssets(int offset, int count) { - assert(offset >= 0); - assert(count > 0); - assert(offset + count <= totalAssets); - if (allAssets != null) { - // if we already loaded all assets (e.g. from search result) - // simply return the requested slice of that array - return allAssets!.slice(offset, offset + count); - } else if (query != null) { - // general case: we have the query to load assets via offset from the DB on demand - if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) { - // the requested slice (offset:offset+count) is not contained in the cache buffer `_buf` - // thus, fill the buffer with a new batch of assets that at least contains the requested - // assets and some more - - final bool forward = _bufOffset < offset; - // if the requested offset is greater than the cached offset, the user scrolls forward "down" - const batchSize = 256; - const oppositeSize = 64; - - // make sure to load a meaningful amount of data (and not only the requested slice) - // otherwise, each call to [loadAssets] would result in DB call trashing performance - // fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests - final len = max(batchSize, count + oppositeSize); - // when scrolling forward, start shortly before the requested offset... - // when scrolling backward, end shortly after the requested offset... - // ... to guard against the user scrolling in the other direction - // a tiny bit resulting in a another required load from the DB - final start = max(0, forward ? offset - oppositeSize : (len > batchSize ? offset : offset + count - len)); - // load the calculated batch (start:start+len) from the DB and put it into the buffer - _buf = query!.offset(start).limit(len).findAllSync(); - _bufOffset = start; - } - assert(_bufOffset <= offset); - assert(_bufOffset + _buf.length >= offset + count); - // return the requested slice from the buffer (we made sure before that the assets are loaded!) - return _buf.slice(offset - _bufOffset, offset - _bufOffset + count); - } - throw Exception("RenderList has neither assets nor query"); - } - - /// Returns the requested asset either from cached buffer or directly from the database - Asset loadAsset(int index) { - if (allAssets != null) { - // all assets are already loaded (e.g. from search result) - return allAssets![index]; - } else if (query != null) { - // general case: we have the DB query to load asset(s) on demand - if (index >= _bufOffset && index < _bufOffset + _buf.length) { - // lucky case: the requested asset is already cached in the buffer! - return _buf[index - _bufOffset]; - } - // request the asset from the database (not changing the buffer!) - final asset = query!.offset(index).findFirstSync(); - if (asset == null) { - throw Exception("Asset at index $index does no longer exist in database"); - } - return asset; - } - throw Exception("RenderList has neither assets nor query"); - } - - static Future fromQuery(QueryBuilder query, GroupAssetsBy groupBy) => - _buildRenderList(null, query, groupBy); - - static Future _buildRenderList( - List? assets, - QueryBuilder? query, - GroupAssetsBy groupBy, - ) async { - final List elements = []; - - const pageSize = 50000; - const sectionSize = 60; // divides evenly by 2,3,4,5,6 - - if (groupBy == GroupAssetsBy.none) { - final int total = assets?.length ?? query!.countSync(); - - final dateLoader = query != null ? DateBatchLoader(query: query, batchSize: 1000 * sectionSize) : null; - - for (int i = 0; i < total; i += sectionSize) { - final date = assets != null ? assets[i].fileCreatedAt : await dateLoader?.getDate(i); - - final int count = i + sectionSize > total ? total - i : sectionSize; - if (date == null) break; - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.assets, - date: date, - count: count, - totalCount: total, - offset: i, - ), - ); - } - return RenderList(elements, query, assets); - } - - final formatSameYear = groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd(); - final formatOtherYear = groupBy == GroupAssetsBy.month ? DateFormat.yMMMM() : DateFormat.yMMMEd(); - final currentYear = DateTime.now().year; - final formatMergedSameYear = DateFormat.MMMd(); - final formatMergedOtherYear = DateFormat.yMMMd(); - - int offset = 0; - DateTime? last; - DateTime? current; - int lastOffset = 0; - int count = 0; - int monthCount = 0; - int lastMonthIndex = 0; - - String formatDateRange(DateTime from, DateTime to) { - final startDate = (from.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(from); - final endDate = (to.year == currentYear ? formatMergedSameYear : formatMergedOtherYear).format(to); - if (DateTime(from.year, from.month, from.day) == DateTime(to.year, to.month, to.day)) { - // format range with time when both dates are on the same day - final startTime = DateFormat.Hm().format(from); - final endTime = DateFormat.Hm().format(to); - return "$startDate $startTime - $endTime"; - } - return "$startDate - $endDate"; - } - - void mergeMonth() { - if (last != null && groupBy == GroupAssetsBy.auto && monthCount <= 30 && elements.length > lastMonthIndex + 1) { - // merge all days into a single section - assert(elements[lastMonthIndex].date.month == last.month); - final e = elements[lastMonthIndex]; - - elements[lastMonthIndex] = RenderAssetGridElement( - RenderAssetGridElementType.monthTitle, - date: e.date, - count: monthCount, - totalCount: monthCount, - offset: e.offset, - title: formatDateRange(e.date, elements.last.date), - ); - elements.removeRange(lastMonthIndex + 1, elements.length); - } - } - - void addElems(DateTime d, DateTime? prevDate) { - final bool newMonth = last == null || last.year != d.year || last.month != d.month; - if (newMonth) { - mergeMonth(); - lastMonthIndex = elements.length; - monthCount = 0; - } - for (int j = 0; j < count; j += sectionSize) { - final type = j == 0 - ? (groupBy != GroupAssetsBy.month && newMonth - ? RenderAssetGridElementType.monthTitle - : RenderAssetGridElementType.groupDividerTitle) - : (groupBy == GroupAssetsBy.auto - ? RenderAssetGridElementType.groupDividerTitle - : RenderAssetGridElementType.assets); - final sectionCount = j + sectionSize > count ? count - j : sectionSize; - assert(sectionCount > 0 && sectionCount <= sectionSize); - elements.add( - RenderAssetGridElement( - type, - date: d, - count: sectionCount, - totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count, - offset: lastOffset + j, - title: j == 0 - ? (d.year == currentYear ? formatSameYear.format(d) : formatOtherYear.format(d)) - : (groupBy == GroupAssetsBy.auto ? formatDateRange(d, prevDate ?? d) : null), - ), - ); - } - monthCount += count; - } - - DateTime? prevDate; - while (true) { - // this iterates all assets (only their createdAt property) in batches - // memory usage is okay, however runtime is linear with number of assets - // TODO replace with groupBy once Isar supports such queries - final dates = assets != null - ? assets.map((a) => a.fileCreatedAt) - : await query!.offset(offset).limit(pageSize).fileCreatedAtProperty().findAll(); - int i = 0; - for (final date in dates) { - final d = DateTime(date.year, date.month, groupBy == GroupAssetsBy.month ? 1 : date.day); - current ??= d; - if (current != d) { - addElems(current, prevDate); - last = current; - current = d; - lastOffset = offset + i; - count = 0; - } - prevDate = date; - count++; - i++; - } - - if (assets != null || dates.length != pageSize) break; - offset += pageSize; - } - if (count > 0 && current != null) { - addElems(current, prevDate); - mergeMonth(); - } - assert(elements.every((e) => e.count <= sectionSize), "too large section"); - return RenderList(elements, query, assets); - } - - static RenderList empty() => RenderList([], null, []); - - static Future fromAssets(List assets, GroupAssetsBy groupBy) => - _buildRenderList(assets, null, groupBy); - - /// Deletes an asset from the render list and clears the buffer - /// This is only a workaround for deleted images still appearing in the gallery - void deleteAsset(Asset deleteAsset) { - allAssets?.remove(deleteAsset); - _buf.clear(); - _bufOffset = 0; - } -} - -class DateBatchLoader { - final QueryBuilder query; - final int batchSize; - - List _buffer = []; - int _bufferStart = 0; - - DateBatchLoader({required this.query, required this.batchSize}); - - Future getDate(int index) async { - if (!_isIndexInBuffer(index)) { - await _loadBatch(index); - } - - if (_isIndexInBuffer(index)) { - return _buffer[index - _bufferStart]; - } - - return null; - } - - Future _loadBatch(int targetIndex) async { - final batchStart = (targetIndex ~/ batchSize) * batchSize; - - _buffer = await query.offset(batchStart).limit(batchSize).fileCreatedAtProperty().findAll(); - - _bufferStart = batchStart; - } - - bool _isIndexInBuffer(int index) { - return index >= _bufferStart && index < _bufferStart + _buffer.length; - } -} diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart deleted file mode 100644 index cd2dc70dae..0000000000 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; - -final controlBottomAppBarNotifier = ControlBottomAppBarNotifier(); - -class ControlBottomAppBarNotifier with ChangeNotifier { - void minimize() { - notifyListeners(); - } -} - -class ControlBottomAppBar extends HookConsumerWidget { - final void Function(bool shareLocal) onShare; - final void Function()? onFavorite; - final void Function()? onArchive; - final void Function([bool force])? onDelete; - final void Function([bool force])? onDeleteServer; - final void Function(bool onlyBackedUp)? onDeleteLocal; - final Function(Album album) onAddToAlbum; - final void Function() onCreateNewAlbum; - final void Function() onUpload; - final void Function()? onStack; - final void Function()? onEditTime; - final void Function()? onEditLocation; - final void Function()? onRemoveFromAlbum; - final void Function()? onToggleLocked; - final void Function()? onDownload; - - final bool enabled; - final bool unfavorite; - final bool unarchive; - final AssetSelectionState selectionAssetState; - final List selectedAssets; - - const ControlBottomAppBar({ - super.key, - required this.onShare, - this.onFavorite, - this.onArchive, - this.onDelete, - this.onDeleteServer, - this.onDeleteLocal, - required this.onAddToAlbum, - required this.onCreateNewAlbum, - required this.onUpload, - this.onDownload, - this.onStack, - this.onEditTime, - this.onEditLocation, - this.onRemoveFromAlbum, - this.onToggleLocked, - this.selectionAssetState = const AssetSelectionState(), - this.selectedAssets = const [], - this.enabled = true, - this.unarchive = false, - this.unfavorite = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasRemote = selectionAssetState.hasRemote || selectionAssetState.hasMerged; - final hasLocal = selectionAssetState.hasLocal || selectionAssetState.hasMerged; - final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final sharedAlbums = ref.watch(albumProvider).where((a) => a.shared).toList(); - const bottomPadding = 0.24; - final scrollController = useDraggableScrollController(); - final isInLockedView = ref.watch(inLockedViewProvider); - - void minimize() { - scrollController.animateTo(bottomPadding, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); - } - - useEffect(() { - controlBottomAppBarNotifier.addListener(minimize); - return () { - controlBottomAppBarNotifier.removeListener(minimize); - }; - }, []); - - void showForceDeleteDialog(Function(bool) deleteCb, {String? alertMsg}) { - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteDialog(alert: alertMsg, onDelete: () => deleteCb(true)); - }, - ); - } - - /// Show existing AddToAlbumBottomSheet - void showAddToAlbumBottomSheet() { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: selectedAssets); - }, - ); - } - - void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) { - if (!force) { - deleteCb(force); - return; - } - return showForceDeleteDialog(deleteCb, alertMsg: alertMsg); - } - - List renderActionButtons() { - return [ - ControlBoxButton( - iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: "share".tr(), - onPressed: enabled ? () => onShare(true) : null, - ), - if (!isInLockedView && hasRemote) - ControlBoxButton( - iconData: Icons.link_rounded, - label: "share_link".tr(), - onPressed: enabled ? () => onShare(false) : null, - ), - if (!isInLockedView && hasRemote && albums.isNotEmpty) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: Icons.photo_album, - label: "add_to_album".tr(), - onPressed: enabled ? showAddToAlbumBottomSheet : null, - ), - ), - if (hasRemote && onArchive != null) - ControlBoxButton( - iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined, - label: (unarchive ? "unarchive" : "archive").tr(), - onPressed: enabled ? onArchive : null, - ), - if (hasRemote && onFavorite != null) - ControlBoxButton( - iconData: unfavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, - label: (unfavorite ? "unfavorite" : "favorite").tr(), - onPressed: enabled ? onFavorite : null, - ), - if (hasRemote && onDownload != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton(iconData: Icons.download, label: "download".tr(), onPressed: onDownload), - ), - if (hasLocal && hasRemote && onDelete != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.delete_sweep_outlined, - label: "delete".tr(), - onPressed: enabled ? () => handleRemoteDelete(!trashEnabled, onDelete!) : null, - onLongPressed: enabled ? () => showForceDeleteDialog(onDelete!) : null, - ), - ), - if (hasRemote && onDeleteServer != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 85), - child: ControlBoxButton( - iconData: Icons.cloud_off_outlined, - label: trashEnabled - ? "control_bottom_app_bar_trash_from_immich".tr() - : "control_bottom_app_bar_delete_from_immich".tr(), - onPressed: enabled - ? () => handleRemoteDelete(!trashEnabled, onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - onLongPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 110), - child: ControlBoxButton( - iconData: Icons.delete_forever, - label: "delete_dialog_title".tr(), - onPressed: enabled - ? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote") - : null, - ), - ), - if (hasLocal && onDeleteLocal != null && !isInLockedView) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.no_cell_outlined, - label: "control_bottom_app_bar_delete_from_local".tr(), - onPressed: enabled - ? () { - if (!selectionAssetState.hasLocal) { - return onDeleteLocal?.call(true); - } - - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteLocalOnlyDialog(onDeleteLocal: onDeleteLocal!); - }, - ); - } - : null, - ), - ), - if (hasRemote && onEditTime != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 95), - child: ControlBoxButton( - iconData: Icons.edit_calendar_outlined, - label: "control_bottom_app_bar_edit_time".tr(), - onPressed: enabled ? onEditTime : null, - ), - ), - if (hasRemote && onEditLocation != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.edit_location_alt_outlined, - label: "control_bottom_app_bar_edit_location".tr(), - onPressed: enabled ? onEditLocation : null, - ), - ), - if (hasRemote) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 100), - child: ControlBoxButton( - iconData: isInLockedView ? Icons.lock_open_rounded : Icons.lock_outline_rounded, - label: isInLockedView ? "remove_from_locked_folder".tr() : "move_to_locked_folder".tr(), - onPressed: enabled ? onToggleLocked : null, - ), - ), - if (!selectionAssetState.hasLocal && selectionAssetState.selectedCount > 1 && onStack != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.filter_none_rounded, - label: "stack".tr(), - onPressed: enabled ? onStack : null, - ), - ), - if (onRemoveFromAlbum != null) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 90), - child: ControlBoxButton( - iconData: Icons.remove_circle_outline, - label: 'remove_from_album'.tr(), - onPressed: enabled ? onRemoveFromAlbum : null, - ), - ), - if (selectionAssetState.hasLocal) - ControlBoxButton( - iconData: Icons.backup_outlined, - label: "upload".tr(), - onPressed: enabled - ? () => showDialog( - context: context, - builder: (BuildContext context) { - return UploadDialog(onUpload: onUpload); - }, - ) - : null, - ), - ]; - } - - getInitialSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.35; - } - return bottomPadding; - } - - getMaxChildSize() { - if (isInLockedView) { - return bottomPadding; - } - if (hasRemote) { - return 0.65; - } - return bottomPadding; - } - - return DraggableScrollableSheet( - initialChildSize: getInitialSize(), - minChildSize: bottomPadding, - maxChildSize: getMaxChildSize(), - snap: true, - controller: scrollController, - builder: (BuildContext context, ScrollController scrollController) { - return Card( - color: context.colorScheme.surfaceContainerHigh, - surfaceTintColor: context.colorScheme.surfaceContainerHigh, - elevation: 6.0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), - ), - margin: const EdgeInsets.all(0), - child: CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: Column( - children: [ - const SizedBox(height: 12), - const CustomDraggingHandle(), - const SizedBox(height: 12), - SizedBox( - height: 120, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: renderActionButtons(), - ), - ), - if (hasRemote && !isInLockedView) ...[ - const Divider(indent: 16, endIndent: 16, thickness: 1), - _AddToAlbumTitleRow(onCreateNewAlbum: enabled ? onCreateNewAlbum : null), - ], - ], - ), - ), - if (hasRemote && !isInLockedView) - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: AddToAlbumSliverList( - albums: albums, - sharedAlbums: sharedAlbums, - onAddToAlbum: onAddToAlbum, - enabled: enabled, - ), - ), - ], - ), - ); - }, - ); - } -} - -class _AddToAlbumTitleRow extends StatelessWidget { - const _AddToAlbumTitleRow({required this.onCreateNewAlbum}); - - final VoidCallback? onCreateNewAlbum; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), - TextButton.icon( - onPressed: onCreateNewAlbum, - icon: Icon(Icons.add, color: context.primaryColor), - label: Text( - "common_create_new_album", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index adb22889a8..ff5aac617a 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,18 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class DeleteDialog extends ConfirmDialog { - const DeleteDialog({super.key, String? alert, required Function onDelete}) - : super( - title: "delete_dialog_title", - content: alert ?? "delete_dialog_alert", - cancel: "cancel", - ok: "delete", - onOk: onDelete, - ); -} class DeleteLocalOnlyDialog extends StatelessWidget { final void Function(bool onlyMerged) onDeleteLocal; diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart deleted file mode 100644 index 93a1d53f4e..0000000000 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class DisableMultiSelectButton extends ConsumerWidget { - const DisableMultiSelectButton({super.key, required this.onPressed, required this.selectedItemCount}); - - final Function onPressed; - final int selectedItemCount; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: ElevatedButton.icon( - onPressed: () => onPressed(), - icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary), - label: Text( - '$selectedItemCount', - style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary), - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart deleted file mode 100644 index 3de52c2816..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart +++ /dev/null @@ -1,559 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(double offsetY); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final CustomScrollView child; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - DraggableScrollbar({ - super.key, - this.alwaysVisibleScrollThumb = false, - required this.heightScrollThumb, - required this.backgroundColor, - required this.scrollThumbBuilder, - required this.child, - required this.controller, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical); - - DraggableScrollbar.rrect({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.arrows({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb); - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbArrowBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = ClipPath( - clipper: const ArrowClipper(), - child: Container( - height: height, - width: 20.0, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - ), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } - - static ScrollThumbBuilder _thumbRRectBuilder(bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(7.0)), - child: Container(constraints: BoxConstraints.tight(Size(16.0, height))), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container(constraints: constraints ?? _defaultConstraints, alignment: Alignment.center, child: child), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late double _viewOffset; - late bool _isDragInProcess; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _viewOffset = 0.0; - _isDragInProcess = false; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb; - - double get barMinScrollExtent => 0; - - double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent; - - double get viewMinScrollExtent => widget.controller.position.minScrollExtent; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_viewOffset + _barOffset + widget.heightScrollThumb / 2); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - //scroll bar has received notification that it's view was scrolled - //so it should also changes his position - //but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - if (notification is ScrollUpdateNotification) { - _barOffset += getBarDelta(notification.scrollDelta!, barMaxScrollExtent, viewMaxScrollExtent); - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - _viewOffset += notification.scrollDelta!; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - }); - } - - double getBarDelta(double scrollViewDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent; - } - - double getScrollViewDelta(double barDelta, double barMaxScrollExtent, double viewMaxScrollExtent) { - return barDelta * viewMaxScrollExtent / barMaxScrollExtent; - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - } - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); - - _viewOffset = widget.controller.position.pixels + viewDelta; - if (_viewOffset < widget.controller.position.minScrollExtent) { - _viewOffset = widget.controller.position.minScrollExtent; - } - if (_viewOffset > viewMaxScrollExtent) { - _viewOffset = viewMaxScrollExtent; - } - widget.controller.jumpTo(_viewOffset); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - setState(() { - _isDragInProcess = false; - }); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart deleted file mode 100644 index 17f35311f0..0000000000 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ /dev/null @@ -1,490 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -/// Build the Scroll Thumb and label using the current configuration -typedef ScrollThumbBuilder = - Widget Function( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }); - -/// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Text Function(int item); - -/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged -/// for quick navigation of the BoxScrollView. -class DraggableScrollbar extends StatefulWidget { - /// The view that will be scrolled with the scroll thumb - final ScrollablePositionedList child; - - final ItemPositionsListener itemPositionsListener; - - /// A function that builds a thumb using the current configuration - final ScrollThumbBuilder scrollThumbBuilder; - - /// The height of the scroll thumb - final double heightScrollThumb; - - /// The background color of the label and thumb - final Color backgroundColor; - - /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry? padding; - - /// The height offset of the thumb/bar from the bottom of the page - final double? heightOffset; - - /// Determines how quickly the scrollbar will animate in and out - final Duration scrollbarAnimationDuration; - - /// How long should the thumb be visible before fading out - final Duration scrollbarTimeToFade; - - /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; - - /// Determines box constraints for Container displaying label - final BoxConstraints? labelConstraints; - - /// The ScrollController for the BoxScrollView - final ItemScrollController controller; - - /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] - final bool alwaysVisibleScrollThumb; - - final Function(bool scrolling) scrollStateListener; - - DraggableScrollbar.semicircle({ - super.key, - Key? scrollThumbKey, - this.alwaysVisibleScrollThumb = false, - required this.child, - required this.controller, - required this.itemPositionsListener, - required this.scrollStateListener, - this.heightScrollThumb = 48.0, - this.backgroundColor = Colors.white, - this.padding, - this.heightOffset, - this.scrollbarAnimationDuration = const Duration(milliseconds: 300), - this.scrollbarTimeToFade = const Duration(milliseconds: 600), - this.labelTextBuilder, - this.labelConstraints, - }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb); - - @override - DraggableScrollbarState createState() => DraggableScrollbarState(); - - static buildScrollThumbAndLabel({ - required Widget scrollThumb, - required Color backgroundColor, - required Animation? thumbAnimation, - required Animation? labelAnimation, - required Text? labelText, - required BoxConstraints? labelConstraints, - required bool alwaysVisibleScrollThumb, - }) { - var scrollThumbAndLabel = labelText == null - ? scrollThumb - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScrollLabel( - animation: labelAnimation, - backgroundColor: backgroundColor, - constraints: labelConstraints, - child: labelText, - ), - scrollThumb, - ], - ); - - if (alwaysVisibleScrollThumb) { - return scrollThumbAndLabel; - } - return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel); - } - - static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { - return ( - Color backgroundColor, - Animation thumbAnimation, - Animation labelAnimation, - double height, { - Text? labelText, - BoxConstraints? labelConstraints, - }) { - final scrollThumb = CustomPaint( - key: scrollThumbKey, - foregroundPainter: ArrowCustomPainter(Colors.white), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(height), - bottomLeft: Radius.circular(height), - topRight: const Radius.circular(4.0), - bottomRight: const Radius.circular(4.0), - ), - child: Container(constraints: BoxConstraints.tight(Size(width, height))), - ), - ); - - return buildScrollThumbAndLabel( - scrollThumb: scrollThumb, - backgroundColor: backgroundColor, - thumbAnimation: thumbAnimation, - labelAnimation: labelAnimation, - labelText: labelText, - labelConstraints: labelConstraints, - alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, - ); - }; - } -} - -class ScrollLabel extends StatelessWidget { - final Animation? animation; - final Color backgroundColor; - final Text child; - - final BoxConstraints? constraints; - static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); - - const ScrollLabel({ - super.key, - required this.child, - required this.animation, - required this.backgroundColor, - this.constraints = _defaultConstraints, - }); - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: animation!, - child: Container( - margin: const EdgeInsets.only(right: 12.0), - child: Material( - elevation: 4.0, - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: Container( - constraints: constraints ?? _defaultConstraints, - padding: const EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: child, - ), - ), - ), - ); - } -} - -class DraggableScrollbarState extends State with TickerProviderStateMixin { - late double _barOffset; - late bool _isDragInProcess; - late int _currentItem; - - late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; - late AnimationController _labelAnimationController; - late Animation _labelAnimation; - Timer? _fadeoutTimer; - - @override - void initState() { - super.initState(); - _barOffset = 0.0; - _isDragInProcess = false; - _currentItem = 0; - - _thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn); - - _labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration); - - _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); - } - - @override - void dispose() { - _thumbAnimationController.dispose(); - _labelAnimationController.dispose(); - _fadeoutTimer?.cancel(); - super.dispose(); - } - - double get barMaxScrollExtent => (context.size?.height ?? 0) - widget.heightScrollThumb - (widget.heightOffset ?? 0); - - double get barMinScrollExtent => 0; - - int get maxItemCount => widget.child.itemCount; - - @override - Widget build(BuildContext context) { - Text? labelText; - if (widget.labelTextBuilder != null && _isDragInProcess) { - labelText = widget.labelTextBuilder!(_currentItem); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //print("LayoutBuilder constraints=$constraints"); - - return NotificationListener( - onNotification: (ScrollNotification notification) { - changePosition(notification); - return false; - }, - child: Stack( - children: [ - RepaintBoundary(child: widget.child), - RepaintBoundary( - child: GestureDetector( - onVerticalDragStart: _onVerticalDragStart, - onVerticalDragUpdate: _onVerticalDragUpdate, - onVerticalDragEnd: _onVerticalDragEnd, - child: Container( - alignment: Alignment.topRight, - margin: EdgeInsets.only(top: _barOffset), - padding: widget.padding, - child: widget.scrollThumbBuilder( - widget.backgroundColor, - _thumbAnimation, - _labelAnimation, - widget.heightScrollThumb, - labelText: labelText, - labelConstraints: widget.labelConstraints, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - // scroll bar has received notification that it's view was scrolled - // so it should also changes his position - // but only if it isn't dragged - changePosition(ScrollNotification notification) { - if (_isDragInProcess) { - return; - } - - setState(() { - try { - int firstItemIndex = widget.itemPositionsListener.itemPositions.value.first.index; - - if (notification is ScrollUpdateNotification) { - _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - - if (itemPosition < maxItemCount) { - _currentItem = itemPosition; - } - - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } - } catch (_) {} - }); - } - - void _onVerticalDragStart(DragStartDetails details) { - setState(() { - _isDragInProcess = true; - _labelAnimationController.forward(); - _fadeoutTimer?.cancel(); - }); - - widget.scrollStateListener(true); - } - - int get itemPosition { - int numberOfItems = widget.child.itemCount; - return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt(); - } - - void _jumpToBarPosition() { - if (itemPosition > maxItemCount - 1) { - return; - } - - _currentItem = itemPosition; - - /// If the bar is at the bottom but the item position is still smaller than the max item count (due to rounding error) - /// jump to the end of the list - if (barMaxScrollExtent - _barOffset < 10 && itemPosition < maxItemCount) { - widget.controller.jumpTo(index: maxItemCount); - - return; - } - - widget.controller.jumpTo(index: itemPosition); - } - - Timer? dragHaltTimer; - int lastTimerPosition = 0; - - void _onVerticalDragUpdate(DragUpdateDetails details) { - setState(() { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); - } - if (_isDragInProcess) { - _barOffset += details.delta.dy; - - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - - if (itemPosition != lastTimerPosition) { - lastTimerPosition = itemPosition; - dragHaltTimer?.cancel(); - widget.scrollStateListener(true); - - dragHaltTimer = Timer(const Duration(milliseconds: 500), () { - widget.scrollStateListener(false); - }); - } - - _jumpToBarPosition(); - } - }); - } - - void _onVerticalDragEnd(DragEndDetails details) { - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - - setState(() { - _jumpToBarPosition(); - _isDragInProcess = false; - }); - - widget.scrollStateListener(false); - } -} - -/// Draws 2 triangles like arrow up and arrow down -class ArrowCustomPainter extends CustomPainter { - Color color; - - ArrowCustomPainter(this.color); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; - const width = 12.0; - const height = 8.0; - final baseX = size.width / 2; - final baseY = size.height / 2; - - canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint); - canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint); - } - - static Path _trianglePath(Offset o, double width, double height, bool isUp) { - return Path() - ..moveTo(o.dx, o.dy) - ..lineTo(o.dx + width, o.dy) - ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) - ..close(); - } -} - -///This cut 2 lines in arrow shape -class ArrowClipper extends CustomClipper { - const ArrowClipper(); - @override - Path getClip(Size size) { - Path path = Path(); - path.lineTo(0.0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0.0); - path.lineTo(0.0, 0.0); - path.close(); - - double arrowWidth = 8.0; - double startPointX = (size.width - arrowWidth) / 2; - double startPointY = size.height / 2 - arrowWidth / 2; - path.moveTo(startPointX, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); - path.lineTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth, startPointY + 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); - path.lineTo(startPointX, startPointY + 1.0); - path.close(); - - startPointY = size.height / 2 + arrowWidth / 2; - path.moveTo(startPointX + arrowWidth, startPointY); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); - path.lineTo(startPointX, startPointY); - path.lineTo(startPointX, startPointY - 1.0); - path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); - path.lineTo(startPointX + arrowWidth, startPointY - 1.0); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} - -class SlideFadeTransition extends StatelessWidget { - final Animation animation; - final Widget child; - - const SlideFadeTransition({super.key, required this.animation, required this.child}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: animation, - builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!, - child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation), - child: FadeTransition(opacity: animation, child: child), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart deleted file mode 100644 index 1464c941f0..0000000000 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; - -class GroupDividerTitle extends HookConsumerWidget { - const GroupDividerTitle({ - super.key, - required this.text, - required this.multiselectEnabled, - required this.onSelect, - required this.onDeselect, - required this.selected, - }); - - final String text; - final bool multiselectEnabled; - final Function onSelect; - final Function onDeselect; - final bool selected; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - final groupBy = useState(GroupAssetsBy.day); - - useEffect(() { - groupBy.value = GroupAssetsBy.values[appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; - return null; - }, []); - - void handleTitleIconClick() { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (selected) { - onDeselect(); - } else { - onSelect(); - } - } - - return Padding( - padding: EdgeInsets.only( - top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0, - bottom: 16.0, - left: 12.0, - right: 12.0, - ), - child: Row( - children: [ - Text( - text, - style: groupBy.value == GroupAssetsBy.month - ? context.textTheme.bodyLarge?.copyWith(fontSize: 24.0) - : context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(250), - fontWeight: FontWeight.w500, - ), - ), - const Spacer(), - GestureDetector( - onTap: handleTitleIconClick, - child: multiselectEnabled && selected - ? Icon( - Icons.check_circle_rounded, - color: context.primaryColor, - semanticLabel: "unselect_all_in".tr(namedArgs: {"group": text}), - ) - : Icon( - Icons.check_circle_outline_rounded, - color: context.colorScheme.onSurfaceSecondary, - semanticLabel: "select_all_in".tr(namedArgs: {"group": text}), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart deleted file mode 100644 index ab6b350a7b..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid_view.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class ImmichAssetGrid extends HookConsumerWidget { - final int? assetsPerRow; - final double margin; - final bool? showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final List? assets; - final RenderList? renderList; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool? dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final bool shrinkWrap; - final bool showDragScroll; - final bool showDragScrollLabel; - final bool showStack; - - const ImmichAssetGrid({ - super.key, - this.assets, - this.onRefresh, - this.renderList, - this.assetsPerRow, - this.showStorageIndicator, - this.listener, - this.margin = 2.0, - this.selectionActive = false, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showDragScrollLabel = true, - this.showStack = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - var settings = ref.watch(appSettingsServiceProvider); - - final perRow = useState(assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!); - final scaleFactor = useState(7.0 - perRow.value); - final baseScaleFactor = useState(7.0 - perRow.value); - - /// assets need different hero tags across tabs / modals - /// otherwise, hero animations are performed across tabs (looks buggy!) - int heroOffset() { - const int range = 1152921504606846976; // 2^60 - final tabScope = TabsRouterScope.of(context); - if (tabScope != null) { - final int tabIndex = tabScope.controller.activeIndex; - return tabIndex * range; - } - return range * 7; - } - - Widget buildAssetGridView(RenderList renderList) { - return RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - baseScaleFactor.value = scaleFactor.value; - }; - - scale.onUpdate = (details) { - scaleFactor.value = max(min(5.0, baseScaleFactor.value * details.scale), 1.0); - if (7 - scaleFactor.value.toInt() != perRow.value) { - perRow.value = 7 - scaleFactor.value.toInt(); - settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value); - } - }; - }, - ), - }, - child: ImmichAssetGridView( - onRefresh: onRefresh, - assetsPerRow: perRow.value, - listener: listener, - showStorageIndicator: showStorageIndicator ?? settings.getSetting(AppSettingsEnum.storageIndicator), - renderList: renderList, - margin: margin, - selectionActive: selectionActive, - preselectedAssets: preselectedAssets, - canDeselect: canDeselect, - dynamicLayout: dynamicLayout ?? settings.getSetting(AppSettingsEnum.dynamicLayout), - showMultiSelectIndicator: showMultiSelectIndicator, - visibleItemsListener: visibleItemsListener, - topWidget: topWidget, - heroOffset: heroOffset(), - shrinkWrap: shrinkWrap, - showDragScroll: showDragScroll, - showStack: showStack, - showLabel: showDragScrollLabel, - ), - ); - } - - if (renderList != null) return buildAssetGridView(renderList!); - - final renderListFuture = ref.watch(assetsTimelineProvider(assets!)); - return renderListFuture.widgetWhen(onData: (renderList) => buildAssetGridView(renderList)); - } -} - -/// accepts a gesture even though it should reject it (because child won) -class CustomScaleGestureRecognizer extends ScaleGestureRecognizer { - @override - void rejectGesture(int pointer) { - acceptGesture(pointer); - } -} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart deleted file mode 100644 index c323c573b4..0000000000 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ /dev/null @@ -1,828 +0,0 @@ -import 'dart:collection'; -import 'dart:developer'; -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/disable_multi_select_button.dart'; -import 'package:immich_mobile/widgets/asset_grid/draggable_scrollbar_custom.dart'; -import 'package:immich_mobile/widgets/asset_grid/group_divider_title.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -typedef ImmichAssetGridSelectionListener = void Function(bool, Set); - -class ImmichAssetGridView extends ConsumerStatefulWidget { - final RenderList renderList; - final int assetsPerRow; - final double margin; - final bool showStorageIndicator; - final ImmichAssetGridSelectionListener? listener; - final bool selectionActive; - final Future Function()? onRefresh; - final Set? preselectedAssets; - final bool canDeselect; - final bool dynamicLayout; - final bool showMultiSelectIndicator; - final void Function(Iterable itemPositions)? visibleItemsListener; - final Widget? topWidget; - final int heroOffset; - final bool shrinkWrap; - final bool showDragScroll; - final bool showStack; - final bool showLabel; - - const ImmichAssetGridView({ - super.key, - required this.renderList, - required this.assetsPerRow, - required this.showStorageIndicator, - this.listener, - this.margin = 5.0, - this.selectionActive = false, - this.onRefresh, - this.preselectedAssets, - this.canDeselect = true, - this.dynamicLayout = true, - this.showMultiSelectIndicator = true, - this.visibleItemsListener, - this.topWidget, - this.heroOffset = 0, - this.shrinkWrap = false, - this.showDragScroll = true, - this.showStack = false, - this.showLabel = true, - }); - - @override - createState() { - return ImmichAssetGridViewState(); - } -} - -class ImmichAssetGridViewState extends ConsumerState { - final ItemScrollController _itemScrollController = ItemScrollController(); - final ScrollOffsetController _scrollOffsetController = ScrollOffsetController(); - final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); - late final KeepAliveLink currentAssetLink; - - /// The timestamp when the haptic feedback was last invoked - int _hapticFeedbackTS = 0; - DateTime? _prevItemTime; - bool _scrolling = false; - final Set _selectedAssets = LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - bool _dragging = false; - int? _dragAnchorAssetIndex; - int? _dragAnchorSectionIndex; - final Set _draggedAssets = HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id); - - ScrollPhysics? _scrollPhysics; - - Set _getSelectedAssets() { - return Set.from(_selectedAssets); - } - - void _callSelectionListener(bool selectionActive) { - widget.listener?.call(selectionActive, _getSelectedAssets()); - } - - void _selectAssets(List assets) { - setState(() { - if (_dragging) { - _draggedAssets.addAll(assets); - } - _selectedAssets.addAll(assets); - _callSelectionListener(true); - }); - } - - void _deselectAssets(List assets) { - final assetsToDeselect = assets.where( - (a) => widget.canDeselect || !(widget.preselectedAssets?.contains(a) ?? false), - ); - - setState(() { - _selectedAssets.removeAll(assetsToDeselect); - if (_dragging) { - _draggedAssets.removeAll(assetsToDeselect); - } - _callSelectionListener(_selectedAssets.isNotEmpty); - }); - } - - void _deselectAll() { - setState(() { - _selectedAssets.clear(); - _dragAnchorAssetIndex = null; - _dragAnchorSectionIndex = null; - _draggedAssets.clear(); - _dragging = false; - if (!widget.canDeselect && widget.preselectedAssets != null && widget.preselectedAssets!.isNotEmpty) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - _callSelectionListener(false); - }); - } - - bool _allAssetsSelected(List assets) { - return widget.selectionActive && assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null; - } - - Future _scrollToIndex(int index) async { - // if the index is so far down, that the end of the list is reached on the screen - // the scroll_position widget crashes. This is a workaround to prevent this. - // If the index is within the last 10 elements, we jump instead of scrolling. - if (widget.renderList.elements.length <= index + 10) { - _itemScrollController.jumpTo(index: index); - return; - } - await _itemScrollController.scrollTo(index: index, alignment: 0, duration: const Duration(milliseconds: 500)); - } - - Widget _itemBuilder(BuildContext c, int position) { - int index = position; - if (widget.topWidget != null) { - if (index == 0) { - return widget.topWidget!; - } - index--; - } - - final section = widget.renderList.elements[index]; - return _Section( - showStorageIndicator: widget.showStorageIndicator, - selectedAssets: _selectedAssets, - selectionActive: widget.selectionActive, - sectionIndex: index, - section: section, - margin: widget.margin, - renderList: widget.renderList, - assetsPerRow: widget.assetsPerRow, - scrolling: _scrolling, - dynamicLayout: widget.dynamicLayout, - selectAssets: _selectAssets, - deselectAssets: _deselectAssets, - allAssetsSelected: _allAssetsSelected, - showStack: widget.showStack, - heroOffset: widget.heroOffset, - onAssetTap: (asset) { - ref.read(currentAssetProvider.notifier).set(asset); - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - if (asset.isVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }, - ); - } - - Text _labelBuilder(int pos) { - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return const Text(""); - } - - final date = widget.renderList.elements[pos % maxLength].date; - - return Text( - DateFormat.yMMMM().format(date), - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ); - } - - Widget _buildMultiSelectIndicator() { - return DisableMultiSelectButton(onPressed: () => _deselectAll(), selectedItemCount: _selectedAssets.length); - } - - Widget _buildAssetGrid() { - final useDragScrolling = widget.showDragScroll && widget.renderList.totalAssets >= 20; - - void dragScrolling(bool active) { - if (active != _scrolling) { - setState(() { - _scrolling = active; - }); - } - } - - bool appBarOffset() { - return (ref.watch(tabProvider).index == 0 && ModalRoute.of(context)?.settings.name == TabControllerRoute.name) || - (ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name); - } - - final listWidget = ScrollablePositionedList.builder( - padding: EdgeInsets.only(top: appBarOffset() ? 60 : 0, bottom: 220), - itemBuilder: _itemBuilder, - itemPositionsListener: _itemPositionsListener, - physics: _scrollPhysics, - itemScrollController: _itemScrollController, - scrollOffsetController: _scrollOffsetController, - itemCount: widget.renderList.elements.length + (widget.topWidget != null ? 1 : 0), - addRepaintBoundaries: true, - shrinkWrap: widget.shrinkWrap, - ); - - final child = (useDragScrolling && ModalRoute.of(context) != null) - ? DraggableScrollbar.semicircle( - scrollStateListener: dragScrolling, - itemPositionsListener: _itemPositionsListener, - controller: _itemScrollController, - backgroundColor: context.isDarkTheme - ? context.colorScheme.primary.darken(amount: .5) - : context.colorScheme.primary, - labelTextBuilder: widget.showLabel ? _labelBuilder : null, - padding: appBarOffset() ? const EdgeInsets.only(top: 60) : const EdgeInsets.only(), - heightOffset: appBarOffset() ? 60 : 0, - labelConstraints: const BoxConstraints(maxHeight: 28), - scrollbarAnimationDuration: const Duration(milliseconds: 300), - scrollbarTimeToFade: const Duration(milliseconds: 1000), - child: listWidget, - ) - : listWidget; - - return widget.onRefresh == null - ? child - : appBarOffset() - ? RefreshIndicator(onRefresh: widget.onRefresh!, edgeOffset: 30, child: child) - : RefreshIndicator(onRefresh: widget.onRefresh!, child: child); - } - - void _scrollToDate() { - final date = scrollToDateNotifierProvider.value; - if (date == null) { - ImmichToast.show( - context: context, - msg: "Scroll To Date failed, date is null.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - return; - } - - // Search for the index of the exact date in the list - var index = widget.renderList.elements.indexWhere( - (e) => e.date.year == date.year && e.date.month == date.month && e.date.day == date.day, - ); - - // If the exact date is not found, the timeline is grouped by month, - // thus we search for the month - if (index == -1) { - index = widget.renderList.elements.indexWhere((e) => e.date.year == date.year && e.date.month == date.month); - } - - if (index < widget.renderList.elements.length) { - // Not sure why the index is shifted, but it works. :3 - _scrollToIndex(index + 1); - } else { - ImmichToast.show( - context: context, - msg: "The date (${DateFormat.yMd().format(date)}) could not be found in the timeline.", - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - } - - @override - void didUpdateWidget(ImmichAssetGridView oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.selectionActive) { - setState(() { - _selectedAssets.clear(); - }); - } - } - - @override - void initState() { - super.initState(); - currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive(); - scrollToTopNotifierProvider.addListener(_scrollToTop); - scrollToDateNotifierProvider.addListener(_scrollToDate); - - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.addListener(_positionListener); - } - if (widget.preselectedAssets != null) { - _selectedAssets.addAll(widget.preselectedAssets!); - } - - _itemPositionsListener.itemPositions.addListener(_hapticsListener); - } - - @override - void dispose() { - scrollToTopNotifierProvider.removeListener(_scrollToTop); - scrollToDateNotifierProvider.removeListener(_scrollToDate); - if (widget.visibleItemsListener != null) { - _itemPositionsListener.itemPositions.removeListener(_positionListener); - } - _itemPositionsListener.itemPositions.removeListener(_hapticsListener); - currentAssetLink.close(); - super.dispose(); - } - - void _positionListener() { - final values = _itemPositionsListener.itemPositions.value; - widget.visibleItemsListener?.call(values); - } - - void _hapticsListener() { - /// throttle interval for the haptic feedback in microseconds. - /// Currently set to 100ms. - const feedbackInterval = 100000; - - final values = _itemPositionsListener.itemPositions.value; - final start = values.firstOrNull; - - if (start != null) { - final pos = start.index; - final maxLength = widget.renderList.elements.length; - if (pos < 0 || pos >= maxLength) { - return; - } - - final date = widget.renderList.elements[pos].date; - - // only provide the feedback if the prev. date is known. - // Otherwise the app would provide the haptic feedback - // on startup. - if (_prevItemTime == null) { - _prevItemTime = date; - } else if (_prevItemTime?.year != date.year || _prevItemTime?.month != date.month) { - _prevItemTime = date; - - final now = Timeline.now; - if (now > (_hapticFeedbackTS + feedbackInterval)) { - _hapticFeedbackTS = now; - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - } - } - } - } - - void _scrollToTop() { - // for some reason, this is necessary as well in order - // to correctly reposition the drag thumb scroll bar - _itemScrollController.jumpTo(index: 0); - _itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 200)); - } - - void _setDragStartIndex(AssetIndex index) { - setState(() { - _scrollPhysics = const ClampingScrollPhysics(); - _dragAnchorAssetIndex = index.rowIndex; - _dragAnchorSectionIndex = index.sectionIndex; - _dragging = true; - }); - } - - void _stopDrag() { - WidgetsBinding.instance.addPostFrameCallback((_) { - // Update the physics post frame to prevent sudden change in physics on iOS. - setState(() { - _scrollPhysics = null; - }); - }); - setState(() { - _dragging = false; - _draggedAssets.clear(); - }); - } - - void _dragDragScroll(ScrollDirection direction) { - _scrollOffsetController.animateScroll( - offset: direction == ScrollDirection.forward ? 175 : -175, - duration: const Duration(milliseconds: 125), - ); - } - - void _handleDragAssetEnter(AssetIndex index) { - if (_dragAnchorSectionIndex == null || _dragAnchorAssetIndex == null) { - return; - } - - final dragAnchorSectionIndex = _dragAnchorSectionIndex!; - final dragAnchorAssetIndex = _dragAnchorAssetIndex!; - - late final int startSectionIndex; - late final int startSectionAssetIndex; - late final int endSectionIndex; - late final int endSectionAssetIndex; - - if (index.sectionIndex < dragAnchorSectionIndex) { - startSectionIndex = index.sectionIndex; - startSectionAssetIndex = index.rowIndex; - endSectionIndex = dragAnchorSectionIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } else if (index.sectionIndex > dragAnchorSectionIndex) { - startSectionIndex = dragAnchorSectionIndex; - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionIndex = index.sectionIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionIndex = dragAnchorSectionIndex; - endSectionIndex = dragAnchorSectionIndex; - - // If same section, assign proper start / end asset Index - if (dragAnchorAssetIndex < index.rowIndex) { - startSectionAssetIndex = dragAnchorAssetIndex; - endSectionAssetIndex = index.rowIndex; - } else { - startSectionAssetIndex = index.rowIndex; - endSectionAssetIndex = dragAnchorAssetIndex; - } - } - - final selectedAssets = {}; - var currentSectionIndex = startSectionIndex; - while (currentSectionIndex < endSectionIndex) { - final section = widget.renderList.elements.elementAtOrNull(currentSectionIndex); - if (section == null) continue; - - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - - if (currentSectionIndex == startSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, sectionAssets.length)); - } else { - selectedAssets.addAll(sectionAssets); - } - - currentSectionIndex += 1; - } - - final section = widget.renderList.elements.elementAtOrNull(endSectionIndex); - if (section != null) { - final sectionAssets = widget.renderList.loadAssets(section.offset, section.count); - if (startSectionIndex == endSectionIndex) { - selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1)); - } else { - selectedAssets.addAll(sectionAssets.slice(0, endSectionAssetIndex + 1)); - } - } - - _deselectAssets(_draggedAssets.toList()); - _draggedAssets.clear(); - _draggedAssets.addAll(selectedAssets); - _selectAssets(_draggedAssets.toList()); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty), - onPopInvokedWithResult: (didPop, _) { - if (didPop) { - return; - } else { - /// `preselectedAssets` is only present when opening the asset grid from the - /// "add to album" button. - /// - /// `_selectedAssets` includes `preselectedAssets` on initialization. - if (_selectedAssets.length > (widget.preselectedAssets?.length ?? 0)) { - /// `_deselectAll` only deselects the selected assets, - /// doesn't affect the preselected ones. - _deselectAll(); - return; - } else { - Navigator.of(context).canPop() ? Navigator.of(context).pop() : null; - } - } - }, - child: Stack( - children: [ - AssetDragRegion( - onStart: _setDragStartIndex, - onAssetEnter: _handleDragAssetEnter, - onEnd: _stopDrag, - onScroll: _dragDragScroll, - onScrollStart: () => - WidgetsBinding.instance.addPostFrameCallback((_) => controlBottomAppBarNotifier.minimize()), - child: _buildAssetGrid(), - ), - if (widget.showMultiSelectIndicator && widget.selectionActive) _buildMultiSelectIndicator(), - ], - ), - ); - } -} - -/// A single row of all placeholder widgets -class _PlaceholderRow extends StatelessWidget { - final int number; - final double width; - final double height; - final double margin; - - const _PlaceholderRow({ - super.key, - required this.number, - required this.width, - required this.height, - required this.margin, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - for (int i = 0; i < number; i++) - ThumbnailPlaceholder( - key: ValueKey(i), - width: width, - height: height, - margin: EdgeInsets.only(bottom: margin, right: i + 1 == number ? 0.0 : margin), - ), - ], - ); - } -} - -/// A section for the render grid -class _Section extends StatelessWidget { - final RenderAssetGridElement section; - final int sectionIndex; - final Set selectedAssets; - final bool scrolling; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool dynamicLayout; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - final bool showStack; - final int heroOffset; - final bool showStorageIndicator; - final void Function(Asset) onAssetTap; - - const _Section({ - required this.section, - required this.sectionIndex, - required this.scrolling, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.dynamicLayout, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - required this.selectedAssets, - required this.showStack, - required this.heroOffset, - required this.showStorageIndicator, - required this.onAssetTap, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth / assetsPerRow - margin * (assetsPerRow - 1) / assetsPerRow; - final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow; - final List assetsToRender = scrolling ? [] : renderList.loadAssets(section.offset, section.count); - return Column( - key: ValueKey(section.offset), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (section.type == RenderAssetGridElementType.monthTitle) _MonthTitle(date: section.date), - if (section.type == RenderAssetGridElementType.groupDividerTitle || - section.type == RenderAssetGridElementType.monthTitle) - _Title( - selectionActive: selectionActive, - title: section.title!, - assets: scrolling ? [] : renderList.loadAssets(section.offset, section.totalCount), - allAssetsSelected: allAssetsSelected, - selectAssets: selectAssets, - deselectAssets: deselectAssets, - ), - for (int i = 0; i < rows; i++) - scrolling - ? _PlaceholderRow( - key: ValueKey(i), - number: i + 1 == rows ? section.count - i * assetsPerRow : assetsPerRow, - width: width, - height: width, - margin: margin, - ) - : _AssetRow( - key: ValueKey(i), - rowStartIndex: i * assetsPerRow, - sectionIndex: sectionIndex, - assets: assetsToRender.nestedSlice(i * assetsPerRow, min((i + 1) * assetsPerRow, section.count)), - absoluteOffset: section.offset + i * assetsPerRow, - width: width, - assetsPerRow: assetsPerRow, - margin: margin, - dynamicLayout: dynamicLayout, - renderList: renderList, - selectedAssets: selectedAssets, - isSelectionActive: selectionActive, - showStack: showStack, - heroOffset: heroOffset, - showStorageIndicator: showStorageIndicator, - selectionActive: selectionActive, - onSelect: (asset) => selectAssets([asset]), - onDeselect: (asset) => deselectAssets([asset]), - onAssetTap: onAssetTap, - ), - ], - ); - }, - ); - } -} - -/// The month title row for a section -class _MonthTitle extends StatelessWidget { - final DateTime date; - - const _MonthTitle({required this.date}); - - @override - Widget build(BuildContext context) { - final monthFormat = DateTime.now().year == date.year ? DateFormat.MMMM() : DateFormat.yMMMM(); - final String title = monthFormat.format(date); - return Padding( - key: Key("month-$title"), - padding: const EdgeInsets.only(left: 12.0, top: 24.0), - child: Text( - toBeginningOfSentenceCase(title, context.locale.languageCode), - style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w500), - ), - ); - } -} - -/// A title row -class _Title extends StatelessWidget { - final String title; - final List assets; - final bool selectionActive; - final void Function(List) selectAssets; - final void Function(List) deselectAssets; - final bool Function(List) allAssetsSelected; - - const _Title({ - required this.title, - required this.assets, - required this.selectionActive, - required this.selectAssets, - required this.deselectAssets, - required this.allAssetsSelected, - }); - - @override - Widget build(BuildContext context) { - return GroupDividerTitle( - text: toBeginningOfSentenceCase(title, context.locale.languageCode), - multiselectEnabled: selectionActive, - onSelect: () => selectAssets(assets), - onDeselect: () => deselectAssets(assets), - selected: allAssetsSelected(assets), - ); - } -} - -/// The row of assets -class _AssetRow extends StatelessWidget { - final List assets; - final int rowStartIndex; - final int sectionIndex; - final Set selectedAssets; - final int absoluteOffset; - final double width; - final bool dynamicLayout; - final double margin; - final int assetsPerRow; - final RenderList renderList; - final bool selectionActive; - final bool showStorageIndicator; - final int heroOffset; - final bool showStack; - final void Function(Asset) onAssetTap; - final void Function(Asset)? onSelect; - final void Function(Asset)? onDeselect; - final bool isSelectionActive; - - const _AssetRow({ - super.key, - required this.rowStartIndex, - required this.sectionIndex, - required this.assets, - required this.absoluteOffset, - required this.width, - required this.dynamicLayout, - required this.margin, - required this.assetsPerRow, - required this.renderList, - required this.selectionActive, - required this.showStorageIndicator, - required this.heroOffset, - required this.showStack, - required this.isSelectionActive, - required this.selectedAssets, - required this.onAssetTap, - this.onSelect, - this.onDeselect, - }); - - @override - Widget build(BuildContext context) { - // Default: All assets have the same width - final widthDistribution = List.filled(assets.length, 1.0); - - if (dynamicLayout) { - final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); - final meanAspectRatio = aspectRatios.sum / assets.length; - - // 1: mean width - // 0.5: width < mean - threshold - // 1.5: width > mean + threshold - final arConfiguration = aspectRatios.map((e) { - if (e - meanAspectRatio > 0.3) return 1.5; - if (e - meanAspectRatio < -0.3) return 0.5; - return 1.0; - }); - - // Normalize: - final sum = arConfiguration.sum; - widthDistribution.setRange(0, widthDistribution.length, arConfiguration.map((e) => (e * assets.length) / sum)); - } - return Row( - key: key, - children: assets.mapIndexed((int index, Asset asset) { - final bool last = index + 1 == assetsPerRow; - final isSelected = isSelectionActive && selectedAssets.contains(asset); - return Container( - width: width * widthDistribution[index], - height: width, - margin: EdgeInsets.only(bottom: margin, right: last ? 0.0 : margin), - child: GestureDetector( - onTap: () { - if (selectionActive) { - if (isSelected) { - onDeselect?.call(asset); - } else { - onSelect?.call(asset); - } - } else { - final asset = renderList.loadAsset(absoluteOffset + index); - onAssetTap(asset); - context.pushRoute( - GalleryViewerRoute( - renderList: renderList, - initialIndex: absoluteOffset + index, - heroOffset: heroOffset, - showStack: showStack, - ), - ); - } - }, - onLongPress: () { - onSelect?.call(asset); - HapticFeedback.heavyImpact(); - }, - child: AssetIndexWrapper( - rowIndex: rowStartIndex + index, - sectionIndex: sectionIndex, - child: ThumbnailImage( - asset: asset, - multiselectEnabled: selectionActive, - isSelected: isSelectionActive && selectedAssets.contains(asset), - showStorageIndicator: showStorageIndicator, - heroOffset: heroOffset, - showStack: showStack, - ), - ), - ), - ); - }).toList(), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart deleted file mode 100644 index c0d8a6bea2..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ /dev/null @@ -1,458 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/asset_selection_state.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/multiselect.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class MultiselectGrid extends HookConsumerWidget { - const MultiselectGrid({ - super.key, - required this.renderListProvider, - this.onRefresh, - this.buildLoadingIndicator, - this.onRemoveFromAlbum, - this.topWidget, - this.stackEnabled = false, - this.dragScrollLabelEnabled = true, - this.archiveEnabled = false, - this.deleteEnabled = true, - this.favoriteEnabled = true, - this.editEnabled = false, - this.unarchive = false, - this.unfavorite = false, - this.downloadEnabled = true, - this.emptyIndicator, - }); - - final ProviderListenable> renderListProvider; - final Future Function()? onRefresh; - final Widget Function()? buildLoadingIndicator; - final Future Function(Iterable)? onRemoveFromAlbum; - final Widget? topWidget; - final bool stackEnabled; - final bool dragScrollLabelEnabled; - final bool archiveEnabled; - final bool unarchive; - final bool deleteEnabled; - final bool downloadEnabled; - final bool favoriteEnabled; - final bool unfavorite; - final bool editEnabled; - final Widget? emptyIndicator; - Widget buildDefaultLoadingIndicator() => const Center(child: CircularProgressIndicator()); - - Widget buildEmptyIndicator() => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final multiselectEnabled = ref.watch(multiselectProvider.notifier); - final selectionEnabledHook = useState(false); - final selectionAssetState = useState(const AssetSelectionState()); - - final selection = useState({}); - final currentUser = ref.watch(currentUserProvider); - final processing = useProcessingOverlay(); - - useEffect(() { - selectionEnabledHook.addListener(() { - multiselectEnabled.state = selectionEnabledHook.value; - }); - - return () { - // This does not work in tests - if (kReleaseMode) { - selectionEnabledHook.dispose(); - } - }; - }, []); - - void selectionListener(bool multiselect, Set selectedAssets) { - selectionEnabledHook.value = multiselect; - selection.value = selectedAssets; - selectionAssetState.value = AssetSelectionState.fromSelection(selectedAssets); - } - - errorBuilder(String? msg) => msg != null && msg.isNotEmpty - ? () => ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM) - : null; - - Iterable ownedRemoteSelection({String? localErrorMessage, String? ownerErrorMessage}) { - final assets = selection.value; - return assets - .remoteOnly(errorCallback: errorBuilder(localErrorMessage)) - .ownedOnly(currentUser, errorCallback: errorBuilder(ownerErrorMessage)); - } - - Iterable remoteSelection({String? errorMessage}) => - selection.value.remoteOnly(errorCallback: errorBuilder(errorMessage)); - - void onShareAssets(bool shareLocal) { - processing.value = true; - if (shareLocal) { - // Share = Download + Send to OS specific share sheet - handleShareAssets(ref, context, selection.value); - } else { - final ids = remoteSelection(errorMessage: "home_page_share_err_local".tr()).map((e) => e.remoteId!); - context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList())); - } - processing.value = false; - selectionEnabledHook.value = false; - } - - void onFavoriteAssets() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - await handleFavoriteAssets(ref, context, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onArchiveAsset() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_archive_err_local'.tr(), - ownerErrorMessage: 'home_page_archive_err_partner'.tr(), - ); - await handleArchiveAssets(ref, context, remoteAssets.toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDelete([bool force = false]) async { - processing.value = true; - try { - final toDelete = selection.value - .ownedOnly(currentUser, errorCallback: errorBuilder('home_page_delete_err_partner'.tr())) - .toList(); - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(toDelete, force: force); - - if (isDeleted) { - ImmichToast.show( - context: context, - msg: force - ? 'assets_deleted_permanently'.tr(namedArgs: {'count': "${selection.value.length}"}) - : 'assets_trashed'.tr(namedArgs: {'count': "${selection.value.length}"}), - gravity: ToastGravity.BOTTOM, - ); - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDeleteLocal(bool isMergedAsset) async { - processing.value = true; - try { - final localAssets = selection.value.where((a) => a.isLocal).toList(); - - final toDelete = isMergedAsset ? localAssets.where((e) => e.storage == AssetState.merged) : localAssets; - - if (toDelete.isEmpty) { - return; - } - - final isDeleted = await ref.read(assetProvider.notifier).deleteLocalAssets(toDelete.toList()); - - if (isDeleted) { - final deletedCount = localAssets.where((e) => !isMergedAsset || e.isRemote).length; - - ImmichToast.show( - context: context, - msg: 'assets_removed_permanently_from_device'.tr(namedArgs: {'count': "$deletedCount"}), - gravity: ToastGravity.BOTTOM, - ); - - selectionEnabledHook.value = false; - } - } finally { - processing.value = false; - } - } - - void onDownload() async { - processing.value = true; - try { - final toDownload = selection.value.toList(); - - final results = await ref.read(downloadStateProvider.notifier).downloadAllAsset(toDownload); - - final totalCount = toDownload.length; - final successCount = results.where((e) => e).length; - final failedCount = totalCount - successCount; - - final msg = failedCount > 0 - ? 'assets_downloaded_failed'.t(context: context, args: {'count': successCount, 'error': failedCount}) - : 'assets_downloaded_successfully'.t(context: context, args: {'count': successCount}); - - ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onDeleteRemote([bool shouldDeletePermanently = false]) async { - processing.value = true; - try { - final toDelete = ownedRemoteSelection( - localErrorMessage: 'home_page_delete_remote_err_local'.tr(), - ownerErrorMessage: 'home_page_delete_err_partner'.tr(), - ).toList(); - - final isDeleted = await ref - .read(assetProvider.notifier) - .deleteRemoteAssets(toDelete, shouldDeletePermanently: shouldDeletePermanently); - if (isDeleted) { - ImmichToast.show( - context: context, - msg: shouldDeletePermanently - ? 'assets_deleted_permanently_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}) - : 'assets_trashed_from_server'.tr(namedArgs: {'count': "${toDelete.length}"}), - gravity: ToastGravity.BOTTOM, - ); - } - } finally { - selectionEnabledHook.value = false; - processing.value = false; - } - } - - void onUpload() { - processing.value = true; - selectionEnabledHook.value = false; - try { - ref - .read(manualUploadProvider.notifier) - .uploadAssets(context, selection.value.where((a) => a.storage == AssetState.local)); - } finally { - processing.value = false; - } - } - - void onAddToAlbum(Album album) async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).addAssets(album, assets); - - if (result != null) { - if (result.alreadyInAlbum.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_conflicts".tr( - namedArgs: { - "album": album.name, - "added": result.successfullyAdded.toString(), - "failed": result.alreadyInAlbum.length.toString(), - }, - ), - ); - } else { - ImmichToast.show( - context: context, - msg: "home_page_add_to_album_success".tr( - namedArgs: {"album": album.name, "added": result.successfullyAdded.toString()}, - ), - toastType: ToastType.success, - ); - } - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onCreateNewAlbum() async { - processing.value = true; - try { - final Iterable assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr()); - if (assets.isEmpty) { - return; - } - final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets); - - if (result != null) { - unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums()); - selectionEnabledHook.value = false; - - unawaited(context.pushRoute(AlbumViewerRoute(albumId: result.id))); - } - } finally { - processing.value = false; - } - } - - void onStack() async { - try { - processing.value = true; - if (!selectionEnabledHook.value || selection.value.length < 2) { - return; - } - - await ref.read(stackServiceProvider).createStack(selection.value.map((e) => e.remoteId!).toList()); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - void onEditTime() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditDateTime(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onEditLocation() async { - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_favorite_err_local'.tr(), - ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), - ); - - if (remoteAssets.isNotEmpty) { - unawaited(handleEditLocation(ref, context, remoteAssets.toList())); - } - } finally { - selectionEnabledHook.value = false; - } - } - - void onToggleLockedVisibility() async { - processing.value = true; - try { - final remoteAssets = ownedRemoteSelection( - localErrorMessage: 'home_page_locked_error_local'.tr(), - ownerErrorMessage: 'home_page_locked_error_partner'.tr(), - ); - if (remoteAssets.isNotEmpty) { - final isInLockedView = ref.read(inLockedViewProvider); - final visibility = isInLockedView ? AssetVisibilityEnum.timeline : AssetVisibilityEnum.locked; - - await handleSetAssetsVisibility(ref, context, visibility, remoteAssets.toList()); - } - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - } - - Future Function() wrapLongRunningFun(Future Function() fun, {bool showOverlay = true}) => () async { - if (showOverlay) processing.value = true; - try { - final result = await fun(); - if (result.runtimeType != bool || result == true) { - selectionEnabledHook.value = false; - } - return result; - } finally { - if (showOverlay) processing.value = false; - } - }; - - return SafeArea( - top: true, - bottom: false, - child: Stack( - children: [ - ref - .watch(renderListProvider) - .when( - data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null) - ? (buildLoadingIndicator ?? buildEmptyIndicator)() - : ImmichAssetGrid( - renderList: data, - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - onRefresh: onRefresh == null ? null : wrapLongRunningFun(onRefresh!, showOverlay: false), - topWidget: topWidget, - showStack: stackEnabled, - showDragScrollLabel: dragScrollLabelEnabled, - ), - error: (error, _) => Center(child: Text(error.toString())), - loading: buildLoadingIndicator ?? buildDefaultLoadingIndicator, - ), - if (selectionEnabledHook.value) - ControlBottomAppBar( - key: const ValueKey("controlBottomAppBar"), - onShare: onShareAssets, - onFavorite: favoriteEnabled ? onFavoriteAssets : null, - onArchive: archiveEnabled ? onArchiveAsset : null, - onDelete: deleteEnabled ? onDelete : null, - onDeleteServer: deleteEnabled ? onDeleteRemote : null, - onDownload: downloadEnabled ? onDownload : null, - - /// local file deletion is allowed irrespective of [deleteEnabled] since it has - /// nothing to do with the state of the asset in the Immich server - onDeleteLocal: onDeleteLocal, - onAddToAlbum: onAddToAlbum, - onCreateNewAlbum: onCreateNewAlbum, - onUpload: onUpload, - enabled: !processing.value, - selectionAssetState: selectionAssetState.value, - selectedAssets: selection.value.toList(), - onStack: stackEnabled ? onStack : null, - onEditTime: editEnabled ? onEditTime : null, - onEditLocation: editEnabled ? onEditLocation : null, - unfavorite: unfavorite, - unarchive: unarchive, - onToggleLocked: onToggleLockedVisibility, - onRemoveFromAlbum: onRemoveFromAlbum != null - ? wrapLongRunningFun(() => onRemoveFromAlbum!(selection.value)) - : null, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart b/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart deleted file mode 100644 index 3a1fa82a28..0000000000 --- a/mobile/lib/widgets/asset_grid/multiselect_grid_status_indicator.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class MultiselectGridStatusIndicator extends HookConsumerWidget { - const MultiselectGridStatusIndicator({super.key, this.buildLoadingIndicator, this.emptyIndicator}); - - final Widget Function()? buildLoadingIndicator; - final Widget? emptyIndicator; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final renderListStatus = ref.watch(renderListStatusProvider); - return switch (renderListStatus) { - RenderListStatusEnum.loading => - buildLoadingIndicator == null - ? const Center(child: DelayedLoadingIndicator(delay: Duration(milliseconds: 500))) - : buildLoadingIndicator!(), - RenderListStatusEnum.empty => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()), - RenderListStatusEnum.error => Center(child: const Text("error_loading_assets").tr()), - RenderListStatusEnum.complete => const SizedBox(), - }; - } -} diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart deleted file mode 100644 index 93385b88b3..0000000000 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -class ThumbnailImage extends StatelessWidget { - /// The asset to show the thumbnail image for - final Asset asset; - - /// Whether to show the storage indicator icont over the image or not - final bool showStorageIndicator; - - /// Whether to show the show stack icon over the image or not - final bool showStack; - - /// Whether to show the checkmark indicating that this image is selected - final bool isSelected; - - /// Can override [isSelected] and never show the selection indicator - final bool multiselectEnabled; - - /// If we are allowed to deselect this image - final bool canDeselect; - - /// The offset index to apply to this hero tag for animation - final int heroOffset; - - const ThumbnailImage({ - super.key, - required this.asset, - this.showStorageIndicator = true, - this.showStack = false, - this.isSelected = false, - this.multiselectEnabled = false, - this.heroOffset = 0, - this.canDeselect = true, - }); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return Stack( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - decoration: BoxDecoration( - border: multiselectEnabled && isSelected - ? canDeselect - ? Border.all(color: assetContainerColor, width: 8) - : const Border( - top: BorderSide(color: Colors.grey, width: 8), - right: BorderSide(color: Colors.grey, width: 8), - bottom: BorderSide(color: Colors.grey, width: 8), - left: BorderSide(color: Colors.grey, width: 8), - ) - : const Border(), - ), - child: Stack( - children: [ - _ImageIcon( - heroOffset: heroOffset, - asset: asset, - assetContainerColor: assetContainerColor, - multiselectEnabled: multiselectEnabled, - canDeselect: canDeselect, - isSelected: isSelected, - ), - if (showStorageIndicator) _StorageIcon(storage: asset.storage), - if (asset.isFavorite) - const Positioned(left: 8, bottom: 5, child: Icon(Icons.favorite, color: Colors.white, size: 16)), - if (asset.isVideo) _VideoIcon(duration: asset.duration), - if (asset.stackCount > 0) _StackIcon(isVideo: asset.isVideo, stackCount: asset.stackCount), - ], - ), - ), - if (multiselectEnabled) - isSelected - ? const Padding( - padding: EdgeInsets.all(3.0), - child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()), - ) - : const Icon(Icons.circle_outlined, color: Colors.white), - ], - ); - } -} - -class _SelectedIcon extends StatelessWidget { - const _SelectedIcon(); - - @override - Widget build(BuildContext context) { - final assetContainerColor = context.isDarkTheme - ? context.primaryColor.darken(amount: 0.6) - : context.primaryColor.lighten(amount: 0.8); - - return DecoratedBox( - decoration: BoxDecoration(shape: BoxShape.circle, color: assetContainerColor), - child: Icon(Icons.check_circle_rounded, color: context.primaryColor), - ); - } -} - -class _VideoIcon extends StatelessWidget { - final Duration duration; - - const _VideoIcon({required this.duration}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: 5, - right: 8, - child: Row( - children: [ - Text( - duration.format(), - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 3), - const Icon(Icons.play_circle_fill_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StackIcon extends StatelessWidget { - final bool isVideo; - final int stackCount; - - const _StackIcon({required this.isVideo, required this.stackCount}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: isVideo ? 28 : 5, - right: 8, - child: Row( - children: [ - if (stackCount > 1) - Text( - "$stackCount", - style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), - ), - if (stackCount > 1) const SizedBox(width: 3), - const Icon(Icons.burst_mode_rounded, color: Colors.white, size: 18), - ], - ), - ); - } -} - -class _StorageIcon extends StatelessWidget { - final AssetState storage; - - const _StorageIcon({required this.storage}); - - @override - Widget build(BuildContext context) { - return switch (storage) { - AssetState.local => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_off_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.remote => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - AssetState.merged => const Positioned( - right: 8, - bottom: 5, - child: Icon( - Icons.cloud_done_outlined, - color: Color.fromRGBO(255, 255, 255, 0.8), - size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], - ), - ), - }; - } -} - -class _ImageIcon extends StatelessWidget { - final int heroOffset; - final Asset asset; - final Color assetContainerColor; - final bool multiselectEnabled; - final bool canDeselect; - final bool isSelected; - - const _ImageIcon({ - required this.heroOffset, - required this.asset, - required this.assetContainerColor, - required this.multiselectEnabled, - required this.canDeselect, - required this.isSelected, - }); - - @override - Widget build(BuildContext context) { - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isDto = asset.id == noDbId; - final image = SizedBox.expand( - child: Hero( - tag: isDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset, - child: Stack( - children: [ - SizedBox.expand(child: ImmichThumbnail(asset: asset, height: 250, width: 250)), - const DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromRGBO(0, 0, 0, 0.1), - Colors.transparent, - Colors.transparent, - Color.fromRGBO(0, 0, 0, 0.1), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [0, 0.3, 0.6, 1], - ), - ), - ), - ], - ), - ), - ); - - if (!multiselectEnabled || !isSelected) { - return image; - } - - return DecoratedBox( - decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey), - child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image), - ); - } -} diff --git a/mobile/lib/widgets/asset_grid/upload_dialog.dart b/mobile/lib/widgets/asset_grid/upload_dialog.dart deleted file mode 100644 index 86e2759566..0000000000 --- a/mobile/lib/widgets/asset_grid/upload_dialog.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; - -class UploadDialog extends ConfirmDialog { - final Function onUpload; - - const UploadDialog({super.key, required this.onUpload}) - : super( - title: 'upload_dialog_title', - content: 'upload_dialog_info', - cancel: 'cancel', - ok: 'upload', - onOk: onUpload, - ); -} diff --git a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart deleted file mode 100644 index 1a3ef3eac3..0000000000 --- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class AdvancedBottomSheet extends HookConsumerWidget { - final Asset assetDetail; - final ScrollController? scrollController; - - const AdvancedBottomSheet({super.key, required this.assetDetail, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SingleChildScrollView( - controller: scrollController, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - child: LayoutBuilder( - builder: (context, constraints) { - // One column - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Align(child: Text("ADVANCED INFO", style: TextStyle(fontSize: 12.0))), - const SizedBox(height: 32.0), - Container( - decoration: BoxDecoration( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[200], - borderRadius: const BorderRadius.all(Radius.circular(15.0)), - ), - child: Padding( - padding: const EdgeInsets.only(right: 16.0, left: 16, top: 8, bottom: 16), - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: assetDetail.toString())).then((_) { - context.scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "Copied to clipboard", - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), - ), - ); - }); - }, - icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor), - ), - ), - SelectableText( - assetDetail.toString(), - style: const TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - fontFamily: "GoogleSansCode", - ), - showCursor: true, - ), - ], - ), - ), - ), - const SizedBox(height: 32.0), - ], - ); - }, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart deleted file mode 100644 index 22a7deffff..0000000000 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ /dev/null @@ -1,362 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/stack.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class BottomGalleryBar extends ConsumerWidget { - final ValueNotifier assetIndex; - final bool showStack; - final ValueNotifier stackIndex; - final ValueNotifier totalAssets; - final PageController controller; - final RenderList renderList; - - const BottomGalleryBar({ - super.key, - required this.showStack, - required this.stackIndex, - required this.assetIndex, - required this.controller, - required this.totalAssets, - required this.renderList, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - final stackId = asset.stackId; - - final stackItems = showStack && stackId != null ? ref.watch(assetStackStateProvider(stackId)) : []; - bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; - final navStack = AutoRouter.of(context).stackData; - final isTrashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final isFromTrash = - isTrashEnabled && navStack.length > 2 && navStack.elementAt(navStack.length - 2).name == TrashRoute.name; - final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; - - void removeAssetFromStack() { - if (stackIndex.value > 0 && showStack && stackId != null) { - ref.read(assetStackStateProvider(stackId).notifier).removeChild(stackIndex.value - 1); - } - } - - void handleDelete() async { - Future onDelete(bool force) async { - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets({asset}, force: force); - if (isDeleted && isStackPrimaryAsset) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - // `assetIndex == totalAssets.value - 1` handle the case of removing the last asset - // to not throw the error when the next preCache index is called - if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) { - // Handle only one asset - await context.maybePop(); - } - - totalAssets.value -= 1; - } - if (isDeleted) { - ref.read(currentAssetProvider.notifier).set(renderList.loadAsset(assetIndex.value)); - } - return isDeleted; - } - - // Asset is trashed - if (isTrashEnabled && !isFromTrash) { - final isDeleted = await onDelete(false); - if (isDeleted) { - // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && asset.isRemote && isStackPrimaryAsset) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_trashed'.tr(), - gravity: ToastGravity.BOTTOM, - ); - } - removeAssetFromStack(); - } - return; - } - - // Asset is permanently removed - unawaited( - showDialog( - context: context, - builder: (BuildContext _) { - return DeleteDialog( - onDelete: () async { - final isDeleted = await onDelete(true); - if (isDeleted) { - removeAssetFromStack(); - } - }, - ); - }, - ), - ); - } - - unStack() async { - if (asset.stackId == null) { - return; - } - - await ref.read(stackServiceProvider).deleteStack(asset.stackId!, stackItems); - } - - void showStackActionItems() { - showModalBottomSheet( - context: context, - enableDrag: false, - builder: (BuildContext ctx) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.filter_none_outlined, size: 18), - onTap: () async { - await unStack(); - ctx.pop(); - await context.maybePop(); - }, - title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ], - ), - ), - ); - }, - ); - } - - shareAsset() { - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - ref.read(downloadStateProvider.notifier).shareAsset(asset, context); - } - - void handleEdit() async { - final image = Image(image: ImmichImage.imageProvider(asset: asset)); - - unawaited( - context.navigator.push( - MaterialPageRoute( - builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false), - ), - ), - ); - } - - handleArchive() { - ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isStackPrimaryAsset) { - context.maybePop(); - return; - } - removeAssetFromStack(); - } - - handleDownload() { - if (asset.isLocal) { - return; - } - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleRemoveFromAlbum() async { - final album = ref.read(currentAlbumProvider); - final bool isSuccess = album != null && await ref.read(albumProvider.notifier).removeAsset(album, [asset]); - - if (isSuccess) { - // Workaround for asset remaining in the gallery - renderList.deleteAsset(asset); - - if (totalAssets.value == 1) { - // Handle empty viewer - await context.maybePop(); - } else { - // changing this also for the last asset causes the parent to rebuild with an error - totalAssets.value -= 1; - } - if (assetIndex.value == totalAssets.value && assetIndex.value > 0) { - // handle the case of removing the last asset in the list - assetIndex.value -= 1; - } - } else { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_remove".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - } - - final List> albumActions = [ - { - BottomNavigationBarItem( - icon: Icon(Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded), - label: 'share'.tr(), - tooltip: 'share'.tr(), - ): (_) => - shareAsset(), - }, - if (asset.isImage && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.tune_outlined), - label: 'edit'.tr(), - tooltip: 'edit'.tr(), - ): (_) => - handleEdit(), - }, - if (isOwner && !isInLockedView) - { - asset.isArchived - ? BottomNavigationBarItem( - icon: const Icon(Icons.unarchive_rounded), - label: 'unarchive'.tr(), - tooltip: 'unarchive'.tr(), - ) - : BottomNavigationBarItem( - icon: const Icon(Icons.archive_outlined), - label: 'archive'.tr(), - tooltip: 'archive'.tr(), - ): (_) => - handleArchive(), - }, - if (isOwner && asset.stackCount > 0 && !isInLockedView) - { - BottomNavigationBarItem( - icon: const Icon(Icons.burst_mode_outlined), - label: 'stack'.tr(), - tooltip: 'stack'.tr(), - ): (_) => - showStackActionItems(), - }, - if (isOwner && !isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.delete_outline), - label: 'delete'.tr(), - tooltip: 'delete'.tr(), - ): (_) => - handleDelete(), - }, - if (!isOwner) - { - BottomNavigationBarItem( - icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), - ): (_) => - handleDownload(), - }, - if (isInAlbum) - { - BottomNavigationBarItem( - icon: const Icon(Icons.remove_circle_outline), - label: 'remove_from_album'.tr(), - tooltip: 'remove_from_album'.tr(), - ): (_) => - handleRemoveFromAlbum(), - }, - ]; - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Colors.black, Colors.transparent], - ), - ), - position: DecorationPosition.background, - child: Padding( - padding: const EdgeInsets.only(top: 40.0), - child: Column( - children: [ - if (asset.isVideo) VideoControls(videoPlayerName: asset.id.toString()), - BottomNavigationBar( - elevation: 0.0, - backgroundColor: Colors.transparent, - unselectedIconTheme: const IconThemeData(color: Colors.white), - selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - selectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3), - unselectedFontSize: 14, - selectedFontSize: 14, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white, - showSelectedLabels: true, - showUnselectedLabels: true, - items: albumActions.map((e) => e.keys.first).toList(growable: false), - onTap: (index) { - albumActions[index].values.first.call(index); - }, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/center_play_button.dart b/mobile/lib/widgets/asset_viewer/center_play_button.dart deleted file mode 100644 index 55d8be8095..0000000000 --- a/mobile/lib/widgets/asset_viewer/center_play_button.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; - -class CenterPlayButton extends StatelessWidget { - const CenterPlayButton({ - super.key, - required this.backgroundColor, - this.iconColor, - required this.show, - required this.isPlaying, - required this.isFinished, - this.onPressed, - }); - - final Color backgroundColor; - final Color? iconColor; - final bool show; - final bool isPlaying; - final bool isFinished; - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) { - return Center( - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: DecoratedBox( - decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: iconColor) - : AnimatedPlayPause(color: iconColor, playing: isPlaying), - onPressed: onPressed, - ), - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart deleted file mode 100644 index 09c0e9d091..0000000000 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/cast/cast_manager_state.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; -import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class CustomVideoPlayerControls extends HookConsumerWidget { - final String videoId; - final Duration hideTimerDuration; - - const CustomVideoPlayerControls({ - super.key, - required this.videoId, - this.hideTimerDuration = const Duration(seconds: 5), - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetIsVideo = ref.watch(currentAssetProvider.select((asset) => asset != null && asset.isVideo)); - final showControls = ref.watch(showControlsProvider); - final status = ref.watch(videoPlayerProvider(videoId).select((value) => value.status)); - - final cast = ref.watch(castProvider); - - // A timer to hide the controls - final hideTimer = useTimer(hideTimerDuration, () { - if (!context.mounted) { - return; - } - final s = ref.read(videoPlayerProvider(videoId)).status; - - // Do not hide on paused - if (s != VideoPlaybackStatus.paused && s != VideoPlaybackStatus.completed && assetIsVideo) { - ref.read(showControlsProvider.notifier).show = false; - } - }); - final showBuffering = status == VideoPlaybackStatus.buffering && !cast.isCasting; - - /// Shows the controls and starts the timer to hide them - void showControlsAndStartHideTimer() { - hideTimer.reset(); - ref.read(showControlsProvider.notifier).show = true; - } - - // When playback starts, reset the hide timer - ref.listen(videoPlayerProvider(videoId).select((v) => v.status), (previous, next) { - if (next == VideoPlaybackStatus.playing) { - hideTimer.reset(); - } - }); - - /// Toggles between playing and pausing depending on the state of the video - void togglePlay() { - showControlsAndStartHideTimer(); - - if (cast.isCasting) { - if (cast.castState == CastState.playing) { - ref.read(castProvider.notifier).pause(); - } else if (cast.castState == CastState.paused) { - ref.read(castProvider.notifier).play(); - } else if (cast.castState == CastState.idle) { - // resend the play command since its finished - final asset = ref.read(currentAssetProvider); - if (asset == null) { - return; - } - ref.read(castProvider.notifier).loadMediaOld(asset, true); - } - return; - } - - final notifier = ref.read(videoPlayerProvider(videoId).notifier); - if (status == VideoPlaybackStatus.playing) { - notifier.pause(); - } else if (status == VideoPlaybackStatus.completed) { - notifier.restart(); - } else { - notifier.play(); - } - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: showControlsAndStartHideTimer, - child: AbsorbPointer( - absorbing: !showControls, - child: Stack( - children: [ - if (showBuffering) - const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) - else - GestureDetector( - onTap: () => ref.read(showControlsProvider.notifier).show = false, - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: status == VideoPlaybackStatus.completed, - isPlaying: - status == VideoPlaybackStatus.playing || (cast.isCasting && cast.castState == CastState.playing), - show: assetIsVideo && showControls, - onPressed: togglePlay, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart deleted file mode 100644 index b0cefd63fa..0000000000 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:logging/logging.dart'; - -class DescriptionInput extends HookConsumerWidget { - DescriptionInput({super.key, required this.asset, this.exifInfo}); - - final Asset asset; - final ExifInfo? exifInfo; - final Logger _log = Logger('DescriptionInput'); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(); - final focusNode = useFocusNode(); - final isFocus = useState(false); - final isTextEmpty = useState(controller.text.isEmpty); - final assetService = ref.watch(assetServiceProvider); - final owner = ref.watch(currentUserProvider); - final hasError = useState(false); - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final hasDescription = useState(false); - final isOwner = fastHash(owner?.id ?? '') == asset.ownerId; - - useEffect(() { - assetService.getDescription(asset).then((value) { - controller.text = value; - hasDescription.value = value.isNotEmpty; - }); - return null; - }, [assetWithExif.value]); - - if (!isOwner && !hasDescription.value) { - return const SizedBox.shrink(); - } - - submitDescription(String description) async { - hasError.value = false; - try { - await assetService.setDescription(asset, description); - controller.text = description; - } catch (error, stack) { - hasError.value = true; - _log.severe("Error updating description", error, stack); - ImmichToast.show(context: context, msg: "description_input_submit_error".tr(), toastType: ToastType.error); - } - } - - Widget? suffixIcon; - if (hasError.value) { - suffixIcon = const Icon(Icons.warning_outlined); - } else if (!isTextEmpty.value && isFocus.value) { - suffixIcon = IconButton( - onPressed: () { - controller.clear(); - isTextEmpty.value = true; - }, - icon: Icon(Icons.cancel_rounded, color: context.colorScheme.onSurfaceSecondary), - splashRadius: 10, - ); - } - - return TextField( - enabled: isOwner, - focusNode: focusNode, - onTap: () => isFocus.value = true, - onChanged: (value) { - isTextEmpty.value = false; - }, - onTapOutside: (a) async { - isFocus.value = false; - focusNode.unfocus(); - - if (exifInfo?.description != controller.text) { - await submitDescription(controller.text); - } - }, - autofocus: false, - maxLines: null, - keyboardType: TextInputType.multiline, - controller: controller, - style: context.textTheme.labelLarge, - decoration: InputDecoration( - hintText: 'description_input_hint_text'.tr(), - border: InputBorder.none, - suffixIcon: suffixIcon, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart deleted file mode 100644 index df8f6593df..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; - -class AssetDateTime extends ConsumerWidget { - final Asset asset; - - const AssetDateTime({super.key, required this.asset}); - - String getDateTimeString(Asset a) { - final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(deltaTime); - final time = DateFormat.jm().format(deltaTime); - return '$date • $time GMT${timeZone.formatAsOffset()}'; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final watchedAsset = ref.watch(assetDetailProvider(asset)); - String formattedDateTime = getDateTimeString(asset); - - void editDateTime() async { - await handleEditDateTime(ref, context, [asset]); - - if (watchedAsset.value != null) { - formattedDateTime = getDateTimeString(watchedAsset.value!); - } - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(formattedDateTime, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)), - if (asset.isRemote) IconButton(onPressed: editDateTime, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart deleted file mode 100644 index f0f9a2efcb..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; - -class AssetDetails extends ConsumerWidget { - final Asset asset; - final ExifInfo? exifInfo; - - const AssetDetails({super.key, required this.asset, this.exifInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - - return Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - FileInfo(asset: asset), - if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart deleted file mode 100644 index 6edf226e8b..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; - -class AssetLocation extends HookConsumerWidget { - final Asset asset; - - const AssetLocation({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - - void editLocation() { - handleEditLocation(ref, context, [assetWithExif.value ?? asset]); - } - - // Guard no lat/lng - if (!hasCoordinates) { - return asset.isRemote - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "add_a_location", - style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), - onTap: editLocation, - ) - : const SizedBox.shrink(); - } - - Widget getLocationName() { - if (exifInfo == null) { - return const SizedBox.shrink(); - } - - final cityName = exifInfo.city; - final stateName = exifInfo.state; - - bool hasLocationName = (cityName != null && stateName != null); - - return hasLocationName - ? Text("$cityName, $stateName", style: context.textTheme.labelLarge) - : const SizedBox.shrink(); - } - - return Padding( - padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote) - IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_outlined), iconSize: 20), - ], - ), - asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), - ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId, markerAssetThumbhash: asset.thumbhash), - const SizedBox(height: 16), - getLocationName(), - Text( - "${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart deleted file mode 100644 index 5ae29d32c7..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CameraInfo extends StatelessWidget { - final ExifInfo exifInfo; - - const CameraInfo({super.key, required this.exifInfo}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.camera, color: textColor.withAlpha(200)), - title: Text("${exifInfo.make} ${exifInfo.model}", style: context.textTheme.labelLarge), - subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null - ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart deleted file mode 100644 index 97c9477c97..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart'; -import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class DetailPanel extends HookConsumerWidget { - final Asset asset; - final ScrollController? scrollController; - - const DetailPanel({super.key, required this.asset, this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListView( - controller: scrollController, - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - AssetDateTime(asset: asset), - asset.isRemote ? DescriptionInput(asset: asset) : const SizedBox.shrink(), - PeopleInfo(asset: asset), - AssetLocation(asset: asset), - AssetDetails(asset: asset), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart deleted file mode 100644 index 78d9ac1776..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/utils/bytes_units.dart'; - -class FileInfo extends StatelessWidget { - final Asset asset; - - const FileInfo({super.key, required this.asset}); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - - final height = asset.orientatedHeight ?? asset.height; - final width = asset.orientatedWidth ?? asset.width; - String resolution = height != null && width != null ? "$width x $height " : ""; - String fileSize = asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo!.fileSize!) : ""; - String text = resolution + fileSize; - final imgSizeString = text.isNotEmpty ? text : null; - - String? title; - String? subtitle; - - if (imgSizeString == null && asset.fileName.isNotEmpty) { - // There is only filename - title = asset.fileName; - } else if (imgSizeString != null && asset.fileName.isNotEmpty) { - // There is both filename and size information - title = asset.fileName; - subtitle = imgSizeString; - } else if (imgSizeString != null && asset.fileName.isEmpty) { - title = imgSizeString; - } else { - return const SizedBox.shrink(); - } - - return ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon(Icons.image, color: textColor.withAlpha(200)), - titleAlignment: ListTileTitleAlignment.center, - title: Text(title, style: context.textTheme.labelLarge), - subtitle: subtitle == null ? null : Text(subtitle), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart deleted file mode 100644 index b96cbc777d..0000000000 --- a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; - -class PeopleInfo extends ConsumerWidget { - final Asset asset; - final EdgeInsets? padding; - - const PeopleInfo({super.key, required this.asset, this.padding}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final peopleProvider = ref.watch(assetPeopleNotifierProvider(asset).notifier); - final people = ref.watch(assetPeopleNotifierProvider(asset)).value?.where((p) => !p.isHidden); - - showPersonNameEditModel(String personId, String personName) { - return showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ).then((_) { - // ensure the people list is up-to-date. - peopleProvider.refresh(); - }); - } - - final curatedPeople = - people - ?.map( - (p) => SearchCuratedContent( - id: p.id, - label: p.name, - subtitle: p.birthDate != null && p.birthDate!.isBefore(asset.fileCreatedAt) - ? formatAge(p.birthDate!, asset.fileCreatedAt) - : null, - ), - ) - .toList() ?? - []; - - return AnimatedCrossFade( - crossFadeState: (people?.isEmpty ?? true) ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 200), - firstChild: Container(), - secondChild: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - children: [ - Padding( - padding: padding ?? EdgeInsets.zero, - child: Align( - alignment: Alignment.topLeft, - child: Text( - "exif_bottom_sheet_people", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: CuratedPeopleRow( - padding: padding, - content: curatedPeople, - onTap: (content, index) { - context - .pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - .then((_) => peopleProvider.refresh()); - }, - onNameTap: (person, index) => {showPersonNameEditModel(person.id, person.label)}, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart deleted file mode 100644 index dcb0334801..0000000000 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/hash.dart'; -import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class GalleryAppBar extends ConsumerWidget { - final void Function() showInfo; - - const GalleryAppBar({super.key, required this.showInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = ref.watch(currentAssetProvider); - if (asset == null) { - return const SizedBox(); - } - final album = ref.watch(currentAlbumProvider); - final isOwner = asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? ''); - final showControls = ref.watch(showControlsProvider); - - final isPartner = ref.watch(partnerSharedWithProvider).map((e) => fastHash(e.id)).contains(asset.ownerId); - - toggleFavorite(Asset asset) => ref.read(assetProvider.notifier).toggleFavorite([asset]); - - handleActivities() { - if (album != null && album.shared && album.remoteId != null) { - context.pushRoute(const ActivitiesRoute()); - } - } - - handleRestore(Asset asset) async { - final result = await ref.read(trashProvider.notifier).restoreAssets([asset]); - - if (result && context.mounted) { - ImmichToast.show(context: context, msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM); - } - } - - handleUpload(Asset asset) { - showDialog( - context: context, - builder: (BuildContext _) { - return UploadDialog( - onUpload: () { - ref.read(manualUploadProvider.notifier).uploadAssets(context, [asset]); - }, - ); - }, - ); - } - - addToAlbum(Asset addToAlbumAsset) { - showModalBottomSheet( - elevation: 0, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet(assets: [addToAlbumAsset]); - }, - ); - } - - handleDownloadAsset() { - ref.read(downloadStateProvider.notifier).downloadAsset(asset); - } - - handleLocateAsset() async { - // Go back to the gallery - await context.maybePop(); - await context.navigateTo(const TabControllerRoute(children: [PhotosRoute()])); - ref.read(tabProvider.notifier).update((state) => state = TabEnum.home); - // Scroll to the asset's date - scrollToDateNotifierProvider.scrollToDate(asset.fileCreatedAt); - } - - return IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: showControls ? 1.0 : 0.0, - child: Container( - color: Colors.black.withValues(alpha: 0.4), - child: TopControlAppBar( - isOwner: isOwner, - isPartner: isPartner, - asset: asset, - onMoreInfoPressed: showInfo, - onLocatePressed: handleLocateAsset, - onFavorite: toggleFavorite, - onRestorePressed: () => handleRestore(asset), - onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, - onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, - onAddToAlbumPressed: () => addToAlbum(asset), - onActivitiesPressed: handleActivities, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart deleted file mode 100644 index f5479ab86e..0000000000 --- a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; - -class MotionPhotoButton extends ConsumerWidget { - const MotionPhotoButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isPlaying = ref.watch(isPlayingMotionVideoProvider); - - return IconButton( - onPressed: () { - ref.read(isPlayingMotionVideoProvider.notifier).toggle(); - }, - icon: isPlaying - ? const Icon(Icons.motion_photos_pause_outlined, color: grey200) - : const Icon(Icons.play_circle_outline_rounded, color: grey200), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart deleted file mode 100644 index 35f3840797..0000000000 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/tab.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; - -class TopControlAppBar extends HookConsumerWidget { - const TopControlAppBar({ - super.key, - required this.asset, - required this.onMoreInfoPressed, - required this.onDownloadPressed, - required this.onLocatePressed, - required this.onAddToAlbumPressed, - required this.onRestorePressed, - required this.onFavorite, - required this.onUploadPressed, - required this.isOwner, - required this.onActivitiesPressed, - required this.isPartner, - }); - - final Asset asset; - final Function onMoreInfoPressed; - final VoidCallback? onUploadPressed; - final VoidCallback? onDownloadPressed; - final VoidCallback onLocatePressed; - final VoidCallback onAddToAlbumPressed; - final VoidCallback onRestorePressed; - final VoidCallback onActivitiesPressed; - final Function(Asset) onFavorite; - final bool isOwner; - final bool isPartner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isInLockedView = ref.watch(inLockedViewProvider); - const double iconSize = 22.0; - final a = ref.watch(assetWatcher(asset)).value ?? asset; - final album = ref.watch(currentAlbumProvider); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected)); - - final comments = album != null && album.remoteId != null && asset.remoteId != null - ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId)) - : 0; - - Widget buildFavoriteButton(a) { - return IconButton( - onPressed: () => onFavorite(a), - icon: Icon(a.isFavorite ? Icons.favorite : Icons.favorite_border, color: Colors.grey[200]), - ); - } - - Widget buildLocateButton() { - return IconButton( - onPressed: () { - onLocatePressed(); - }, - icon: Icon(Icons.image_search, color: Colors.grey[200]), - ); - } - - Widget buildMoreInfoButton() { - return IconButton( - onPressed: () { - onMoreInfoPressed(); - }, - icon: Icon(Icons.info_outline_rounded, color: Colors.grey[200]), - ); - } - - Widget buildDownloadButton() { - return IconButton( - onPressed: onDownloadPressed, - icon: Icon(Icons.cloud_download_outlined, color: Colors.grey[200]), - ); - } - - Widget buildAddToAlbumButton() { - return IconButton( - onPressed: () { - onAddToAlbumPressed(); - }, - icon: Icon(Icons.add, color: Colors.grey[200]), - ); - } - - Widget buildRestoreButton() { - return IconButton( - onPressed: () { - onRestorePressed(); - }, - icon: Icon(Icons.history_rounded, color: Colors.grey[200]), - ); - } - - Widget buildActivitiesButton() { - return IconButton( - onPressed: () { - onActivitiesPressed(); - }, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.mode_comment_outlined, color: Colors.grey[200]), - if (comments != 0) - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - comments.toString(), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey[200]), - ), - ), - ], - ), - ); - } - - Widget buildUploadButton() { - return IconButton( - onPressed: onUploadPressed, - icon: Icon(Icons.backup_outlined, color: Colors.grey[200]), - ); - } - - Widget buildBackButton() { - return IconButton( - onPressed: () { - context.maybePop(); - }, - icon: Icon(Icons.arrow_back_ios_new_rounded, size: 20.0, color: Colors.grey[200]), - ); - } - - Widget buildCastButton() { - return IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon( - isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - size: 20.0, - color: isCasting ? context.primaryColor : Colors.grey[200], - ), - ); - } - - bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home; - bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed; - - return AppBar( - foregroundColor: Colors.grey[100], - backgroundColor: Colors.transparent, - leading: buildBackButton(), - actionsIconTheme: const IconThemeData(size: iconSize), - shape: const Border(), - actions: [ - if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (isOwner && !isInHomePage && !(isInTrash ?? false) && !isInLockedView) buildLocateButton(), - if (asset.livePhotoVideoId != null) const MotionPhotoButton(), - if (asset.isLocal && !asset.isRemote) buildUploadButton(), - if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), - if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed && !isInLockedView) buildAddToAlbumButton(), - if (isCasting || (asset.isRemote && websocketConnected)) buildCastButton(), - if (asset.isTrashed) buildRestoreButton(), - if (album != null && album.shared && !isInLockedView) buildActivitiesButton(), - buildMoreInfoButton(), - ], - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart deleted file mode 100644 index d635e136bc..0000000000 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoCard extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoCard({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - final isDarkTheme = context.isDarkTheme; - - ColorFilter selectedFilter = ColorFilter.mode(context.primaryColor.withAlpha(100), BlendMode.darken); - ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); - ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color); - - buildSelectedTextBox() { - if (isSelected) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_included", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: context.primaryColor, - ); - } else if (isExcluded) { - return Chip( - visualDensity: VisualDensity.compact, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - label: Text( - "album_info_card_backup_album_excluded", - style: TextStyle( - fontSize: 10, - color: isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - ).tr(), - backgroundColor: Colors.red[300], - ); - } - - return const SizedBox(); - } - - buildImageFilter() { - if (isSelected) { - return selectedFilter; - } else if (isExcluded) { - return excludedFilter; - } else { - return unselectedFilter; - } - } - - return GestureDetector( - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - onDoubleTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: Card( - clipBehavior: Clip.hardEdge, - margin: const EdgeInsets.all(1), - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.circular(12), // if you need this - ), - side: BorderSide( - color: isDarkTheme ? const Color.fromARGB(255, 37, 35, 35) : const Color(0xFFC9C9C9), - width: 1, - ), - ), - elevation: 0, - borderOnForeground: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Stack( - clipBehavior: Clip.hardEdge, - children: [ - ColorFiltered( - colorFilter: buildImageFilter(), - child: const Image( - width: double.infinity, - height: double.infinity, - image: AssetImage('assets/immich-logo.png'), - fit: BoxFit.cover, - ), - ), - Positioned(bottom: 10, right: 25, child: buildSelectedTextBox()), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - album.name, - style: TextStyle(fontSize: 14, color: context.primaryColor, fontWeight: FontWeight.bold), - ), - Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ), - ], - ), - ), - IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart deleted file mode 100644 index 9796f45e8b..0000000000 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/available_album.model.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class AlbumInfoListTile extends HookConsumerWidget { - final AvailableAlbum album; - - const AlbumInfoListTile({super.key, required this.album}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(album); - final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); - - buildTileColor() { - if (isSelected) { - return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); - } else if (isExcluded) { - return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150); - } else { - return Colors.transparent; - } - } - - buildIcon() { - if (isSelected) { - return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary); - } - - if (isExcluded) { - return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error); - } - - return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest); - } - - return GestureDetector( - onDoubleTap: () { - ref.watch(hapticFeedbackProvider.notifier).selectionClick(); - - if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); - } else { - // Add to exclude album list - - if (album.id == 'isAll' || album.name == 'Recents') { - ImmichToast.show( - context: context, - msg: 'Cannot exclude album contains all assets', - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); - } - }, - child: ListTile( - tileColor: buildTileColor(), - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - onTap: () { - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); - } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (syncAlbum) { - ref.read(albumProvider.notifier).createSyncAlbum(album.name); - } - } - }, - leading: buildIcon(), - title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(album.assetCount.toString()), - trailing: IconButton( - onPressed: () { - context.pushRoute(AlbumPreviewRoute(album: album.album)); - }, - icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), - splashRadius: 25, - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/asset_info_table.dart b/mobile/lib/widgets/backup/asset_info_table.dart deleted file mode 100644 index 2cccded2bb..0000000000 --- a/mobile/lib/widgets/backup/asset_info_table.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupAssetInfoTable extends ConsumerWidget { - const BackupAssetInfoTable({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isUploadInProgress = ref.watch( - backupProvider.select( - (value) => - value.backupProgress == BackUpProgressEnum.inProgress || - value.backupProgress == BackUpProgressEnum.inBackground || - value.backupProgress == BackUpProgressEnum.manualInProgress, - ), - ); - - final asset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Table( - border: TableBorder.all(color: context.colorScheme.outlineVariant, width: 1), - children: [ - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: - Text( - 'backup_controller_page_filename', - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr( - namedArgs: isUploadInProgress - ? {'filename': asset.fileName, 'size': asset.fileType.toLowerCase()} - : {'filename': "-", 'size': "-"}, - ), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - verticalAlignment: TableCellVerticalAlignment.middle, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_created", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-"}), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - "backup_controller_page_id", - style: TextStyle( - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - fontSize: 10.0, - ), - ).tr(namedArgs: {'id': isUploadInProgress ? asset.id : "-"}), - ), - ), - ], - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _getAssetCreationDate(CurrentUploadAsset asset) { - return DateFormat.yMMMMd().format(asset.fileCreatedAt.toLocal()); - } -} diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart deleted file mode 100644 index c2f94e706a..0000000000 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; -import 'package:immich_mobile/widgets/backup/error_chip.dart'; -import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; -import 'package:immich_mobile/widgets/backup/upload_stats.dart'; - -class CurrentUploadingAssetInfoBox extends StatelessWidget { - const CurrentUploadingAssetInfoBox({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: Icon(Icons.image_outlined, color: context.primaryColor, size: 30), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("backup_controller_page_uploading_file_info", style: context.textTheme.titleSmall).tr(), - const BackupErrorChip(), - ], - ), - subtitle: Column( - children: [ - if (Platform.isIOS) const IcloudDownloadProgressBar(), - const BackupUploadProgressBar(), - const BackupUploadStats(), - const BackupAssetInfoTable(), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart deleted file mode 100644 index 191049cd75..0000000000 --- a/mobile/lib/widgets/backup/error_chip.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; - -class BackupErrorChip extends ConsumerWidget { - const BackupErrorChip({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final hasErrors = ref.watch(errorBackupListProvider.select((value) => value.isNotEmpty)); - if (!hasErrors) { - return const SizedBox(); - } - - return ActionChip( - avatar: const Icon(Icons.info, color: red400), - elevation: 1, - visualDensity: VisualDensity.compact, - label: const BackupErrorChipText(), - backgroundColor: Colors.white, - onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), - ); - } -} diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart deleted file mode 100644 index c987dfd331..0000000000 --- a/mobile/lib/widgets/backup/error_chip_text.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; - -class BackupErrorChipText extends ConsumerWidget { - const BackupErrorChipText({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final count = ref.watch(errorBackupListProvider).length; - if (count == 0) { - return const SizedBox(); - } - - return const Text( - "backup_controller_page_failed", - style: TextStyle(color: red400, fontWeight: FontWeight.bold, fontSize: 11), - ).tr(namedArgs: {'count': count.toString()}); - } -} diff --git a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart b/mobile/lib/widgets/backup/icloud_download_progress_bar.dart deleted file mode 100644 index 9f0f7ec3eb..0000000000 --- a/mobile/lib/widgets/backup/icloud_download_progress_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class IcloudDownloadProgressBar extends ConsumerWidget { - const IcloudDownloadProgressBar({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - if (!isIcloudAsset) { - return const SizedBox(); - } - - final iCloudDownloadProgress = ref.watch(backupProvider.select((value) => value.iCloudDownloadProgress)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - SizedBox(width: 110, child: Text("iCloud Download", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: iCloudDownloadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text(" ${iCloudDownloadProgress ~/ 1}%", style: const TextStyle(fontSize: 12)), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart deleted file mode 100644 index be333c6460..0000000000 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; - -/// This is a simple debug widget which should be removed later on when we are -/// more confident about background sync -class IosDebugInfoTile extends HookConsumerWidget { - final IOSBackgroundSettings settings; - const IosDebugInfoTile({super.key, required this.settings}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fetch = settings.timeOfLastFetch; - final processing = settings.timeOfLastProcessing; - final processes = settings.numberOfBackgroundTasksQueued; - - final String title; - if (processes == 0) { - title = 'ios_debug_info_no_processes_queued'.t(context: context); - } else { - title = 'ios_debug_info_processes_queued'.t(context: context, args: {'count': processes}); - } - - final df = DateFormat.yMd().add_jm(); - final String subtitle; - if (fetch == null && processing == null) { - subtitle = 'ios_debug_info_no_sync_yet'.t(context: context); - } else if (fetch != null && processing == null) { - subtitle = 'ios_debug_info_fetch_ran_at'.t(context: context, args: {'dateTime': df.format(fetch)}); - } else if (processing != null && fetch == null) { - subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)}); - } else { - final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'ios_debug_info_last_sync_at'.t(context: context, args: {'dateTime': df.format(fetchOrProcessing)}); - } - - return ListTile( - title: Text( - title, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: context.primaryColor), - ), - subtitle: Text(subtitle, style: const TextStyle(fontSize: 14)), - leading: Icon(Icons.bug_report, color: context.primaryColor), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart deleted file mode 100644 index 641ed14878..0000000000 --- a/mobile/lib/widgets/backup/upload_progress_bar.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadProgressBar extends ConsumerWidget { - const BackupUploadProgressBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final isIcloudAsset = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset)) - : ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset)); - - final uploadProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInPercentage)) - : ref.watch(backupProvider.select((value) => value.progressInPercentage)); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - if (isIcloudAsset) SizedBox(width: 110, child: Text("Immich Upload", style: context.textTheme.labelSmall)), - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Text( - " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart deleted file mode 100644 index 38f99e53fc..0000000000 --- a/mobile/lib/widgets/backup/upload_stats.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; - -class BackupUploadStats extends ConsumerWidget { - const BackupUploadStats({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isManualUpload = ref.watch( - backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress), - ); - - final uploadFileProgress = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSize)) - : ref.watch(backupProvider.select((value) => value.progressInFileSize)); - - final uploadFileSpeed = isManualUpload - ? ref.watch(manualUploadProvider.select((value) => value.progressInFileSpeed)) - : ref.watch(backupProvider.select((value) => value.progressInFileSpeed)); - - return Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")), - Text( - _formatUploadFileSpeed(uploadFileSpeed), - style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"), - ), - ], - ), - ); - } - - @pragma('vm:prefer-inline') - String _formatUploadFileSpeed(double uploadFileSpeed) { - if (uploadFileSpeed < 1024) { - return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; - } else if (uploadFileSpeed < 1024 * 1024) { - return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; - } else if (uploadFileSpeed < 1024 * 1024 * 1024) { - return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; - } else { - return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; - } - } -} diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index c330fb4649..c6c6b2cff1 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -5,18 +5,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; +import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; @@ -32,7 +29,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - BackUpState backupState = ref.watch(backupProvider); + ServerDiskInfo backupState = ref.watch(backupProvider); final theme = context.themeData; bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; @@ -128,9 +125,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - unawaited(ref.read(assetProvider.notifier).clearAllAssets()); ref.read(websocketProvider.notifier).disconnect(); unawaited(context.replaceRoute(const LoginRoute())); }, @@ -146,9 +140,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { } Widget buildStorageInformation() { - var percentage = backupState.serverInfo.diskUsagePercentage / 100; - var usedDiskSpace = backupState.serverInfo.diskUse; - var totalDiskSpace = backupState.serverInfo.diskSize; + var percentage = backupState.diskUsagePercentage / 100; + var usedDiskSpace = backupState.diskUse; + var totalDiskSpace = backupState.diskSize; if (user != null && user.hasQuota) { usedDiskSpace = formatBytes(user.quotaUsageInBytes); @@ -275,7 +269,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ], ), ), - if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), + if (isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), buildFreeUpSpaceButton(), buildSettingButton(), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index a9fdb9a43f..d6881f519a 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -62,10 +61,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget { } void toggleReadonlyMode() { - // read only mode is only supported int he beta experience - // TODO: remove this check when the beta UI goes stable - if (!Store.isBetaTimelineEnabled) return; - final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); ref.read(readonlyModeProvider.notifier).toggleReadonlyMode(); diff --git a/mobile/lib/widgets/common/drag_sheet.dart b/mobile/lib/widgets/common/drag_sheet.dart deleted file mode 100644 index 5d1fda1beb..0000000000 --- a/mobile/lib/widgets/common/drag_sheet.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class CustomDraggingHandle extends StatelessWidget { - const CustomDraggingHandle({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 4, - width: 30, - decoration: BoxDecoration( - color: context.themeData.dividerColor, - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - ); - } -} - -class ControlBoxButton extends StatelessWidget { - const ControlBoxButton({super.key, required this.label, required this.iconData, this.onPressed, this.onLongPressed}); - - final String label; - final IconData iconData; - final void Function()? onPressed; - final void Function()? onLongPressed; - - @override - Widget build(BuildContext context) { - final minWidth = context.isMobile ? MediaQuery.sizeOf(context).width / 4.5 : 75.0; - - return MaterialButton( - padding: const EdgeInsets.all(10), - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), - onPressed: onPressed, - onLongPress: onLongPressed, - minWidth: minWidth, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(iconData, size: 24), - const SizedBox(height: 8), - Text( - label, - style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), - maxLines: 3, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart deleted file mode 100644 index 56b7e91eec..0000000000 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/backup/backup_state.model.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; -import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; - -class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - final List? actions; - final bool showUploadButton; - - const ImmichAppBar({super.key, this.actions, this.showUploadButton = true}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final BackUpState backupState = ref.watch(backupProvider); - final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; - final user = ref.watch(currentUserProvider); - final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user)); - final isDarkTheme = context.isDarkTheme; - const widgetSize = 30.0; - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - buildProfileIndicator() { - return InkWell( - onTap: () => - showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)), - child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2), - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: versionWarningPresent, - offset: const Offset(-2, -12), - child: user == null - ? const Icon(Icons.face_outlined, size: widgetSize) - : Semantics( - label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(size: 32, user: user), - ), - ), - ); - } - - getBackupBadgeIcon() { - final iconColor = isDarkTheme ? Colors.white : Colors.black; - - if (isEnableAutoBackup) { - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - return Container( - padding: const EdgeInsets.all(3.5), - child: CircularProgressIndicator( - strokeWidth: 2, - strokeCap: StrokeCap.round, - valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), - ), - ); - } else if (backupState.backupProgress != BackUpProgressEnum.inBackground && - backupState.backupProgress != BackUpProgressEnum.manualInProgress) { - return Icon( - Icons.check_outlined, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - if (!isEnableAutoBackup) { - return Icon( - Icons.cloud_off_rounded, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), - ); - } - } - - buildBackupIndicator() { - final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = context.colorScheme.surfaceContainer; - - return InkWell( - onTap: () => context.pushRoute(const BackupControllerRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - width: widgetSize / 2, - height: widgetSize / 2, - decoration: BoxDecoration( - color: badgeBackground, - border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)), - borderRadius: BorderRadius.circular(widgetSize / 2), - ), - child: indicatorIcon, - ), - backgroundColor: Colors.transparent, - alignment: Alignment.bottomRight, - isLabelVisible: indicatorIcon != null, - offset: const Offset(-2, -12), - child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor), - ), - ); - } - - return AppBar( - backgroundColor: context.themeData.appBarTheme.backgroundColor, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - automaticallyImplyLeading: false, - centerTitle: false, - title: Builder( - builder: (BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ), - const Tooltip( - triggerMode: TooltipTriggerMode.tap, - showDuration: Duration(seconds: 4), - message: - "The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.", - child: Padding( - padding: EdgeInsets.only(top: 3.0), - child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20), - ), - ), - ], - ); - }, - ), - actions: [ - if (actions != null) - ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (isCasting) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded), - ), - ), - if (showUploadButton) Padding(padding: const EdgeInsets.only(right: 20), child: buildBackupIndicator()), - Padding(padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator()), - ], - ); - } -} diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart deleted file mode 100644 index 57978e83ff..0000000000 --- a/mobile/lib/widgets/common/immich_image.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; - -class ImmichImage extends StatelessWidget { - const ImmichImage( - this.asset, { - this.width, - this.height, - this.fit = BoxFit.cover, - this.placeholder = const ThumbnailPlaceholder(), - super.key, - }); - - final Asset? asset; - final Widget? placeholder; - final double? width; - final double? height; - final BoxFit fit; - - // Helper function to return the image provider for the asset - // either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, double width = 1080, double height = 1920}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteFullImageProvider( - assetId: assetId!, - thumbhash: '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - - if (useLocal(asset)) { - return LocalFullImageProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(width, height), - isAnimated: false, - ); - } else { - return RemoteFullImageProvider( - assetId: asset.remoteId!, - thumbhash: asset.thumbhash ?? '', - assetType: base_asset.AssetType.video, - isAnimated: false, - ); - } - } - - // Whether to use the local asset image provider or a remote one - static bool useLocal(Asset asset) => - !asset.isRemote || asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); - - @override - Widget build(BuildContext context) { - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final imageProviderInstance = ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height); - - return OctoImage( - fadeInDuration: const Duration(milliseconds: 0), - fadeOutDuration: const Duration(milliseconds: 100), - placeholderBuilder: (context) { - if (placeholder != null) { - return placeholder!; - } - return const SizedBox(); - }, - image: imageProviderInstance, - width: width, - height: height, - fit: fit, - errorBuilder: (context, error, stackTrace) { - imageProviderInstance.evict(); - - return Icon(Icons.image_not_supported_outlined, size: 32, color: Colors.red[200]); - }, - ); - } -} diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart deleted file mode 100644 index f17353c3aa..0000000000 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; -import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; -import 'package:octo_image/octo_image.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; - -class ImmichThumbnail extends HookConsumerWidget { - const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key}); - - final Asset? asset; - final double width; - final double height; - final BoxFit fit; - - /// Helper function to return the image provider for the asset thumbnail - /// either by using the asset ID or the asset itself - /// [asset] is the Asset to request, or else use [assetId] to get a remote - /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, int thumbnailSize = 256}) { - if (asset == null && assetId == null) { - throw Exception('Must supply either asset or assetId'); - } - - if (asset == null) { - return RemoteImageProvider.thumbnail(assetId: assetId!, thumbhash: ""); - } - - if (ImmichImage.useLocal(asset)) { - return LocalThumbProvider( - id: asset.localId!, - assetType: base_asset.AssetType.video, - size: Size(thumbnailSize.toDouble(), thumbnailSize.toDouble()), - ); - } else { - return RemoteImageProvider.thumbnail(assetId: asset.remoteId!, thumbhash: asset.thumbhash ?? ""); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - Uint8List? blurhash = useBlurHashRef(asset).value; - - if (asset == null) { - return Container( - color: Colors.grey, - width: width, - height: height, - child: const Center(child: Icon(Icons.no_photography)), - ); - } - - final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []); - - final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset); - - customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { - thumbnailProviderInstance.evict(); - - final originalErrorWidgetBuilder = blurHashErrorBuilder(blurhash, fit: fit); - return originalErrorWidgetBuilder(ctx, error, stackTrace); - } - - return Semantics( - label: assetAltText, - child: OctoImage.fromSet( - placeholderFadeInDuration: Duration.zero, - fadeInDuration: Duration.zero, - fadeOutDuration: const Duration(milliseconds: 100), - octoSet: OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: customErrorBuilder, - ), - image: thumbnailProviderInstance, - width: width, - height: height, - fit: fit, - ), - ); - } -} diff --git a/mobile/lib/widgets/common/share_dialog.dart b/mobile/lib/widgets/common/share_dialog.dart deleted file mode 100644 index 625390c4b7..0000000000 --- a/mobile/lib/widgets/common/share_dialog.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class ShareDialog extends StatelessWidget { - const ShareDialog({super.key}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 0cb1222989..8a9c2eb928 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -4,15 +4,6 @@ import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart'; import 'package:octo_image/octo_image.dart'; -/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as -/// placeholder and [OctoError.icon] as error. -OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) { - return OctoSet( - placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), - ); -} - OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) { return (context) => blurhash == null ? const ThumbnailPlaceholder() diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 179b05a712..7ed9fa5f1c 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -1,14 +1,11 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -64,10 +61,6 @@ class ChangePasswordForm extends HookConsumerWidget { if (isSuccess) { await ref.read(authProvider.notifier).logout(); - - ref.read(manualUploadProvider.notifier).cancelBackup(); - ref.read(backupProvider.notifier).cancelBackup(); - await ref.read(assetProvider.notifier).clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); AutoRouter.of(context).back(); diff --git a/mobile/lib/widgets/forms/login/email_input.dart b/mobile/lib/widgets/forms/login/email_input.dart deleted file mode 100644 index 4d90d918ac..0000000000 --- a/mobile/lib/widgets/forms/login/email_input.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class EmailInput extends StatelessWidget { - final TextEditingController controller; - final FocusNode? focusNode; - final Function()? onSubmit; - - const EmailInput({super.key, required this.controller, this.focusNode, this.onSubmit}); - - String? _validateInput(String? email) { - if (email == null || email == '') return null; - if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); - if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); - if (email.contains(' ') || !email.contains('@')) { - return 'login_form_err_invalid_email'.tr(); - } - return null; - } - - @override - Widget build(BuildContext context) { - return TextFormField( - autofocus: true, - controller: controller, - decoration: InputDecoration( - labelText: 'email'.tr(), - border: const OutlineInputBorder(), - hintText: 'login_form_email_hint'.tr(), - hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - ), - validator: _validateInput, - autovalidateMode: AutovalidateMode.always, - autofillHints: const [AutofillHints.email], - keyboardType: TextInputType.emailAddress, - onFieldSubmitted: (_) => onSubmit?.call(), - focusNode: focusNode, - textInputAction: TextInputAction.next, - ); - } -} diff --git a/mobile/lib/widgets/forms/login/loading_icon.dart b/mobile/lib/widgets/forms/login/loading_icon.dart deleted file mode 100644 index 052ce43ac7..0000000000 --- a/mobile/lib/widgets/forms/login/loading_icon.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoadingIcon extends StatelessWidget { - const LoadingIcon({super.key}); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only(top: 18.0), - child: SizedBox(width: 24, height: 24, child: FittedBox(child: CircularProgressIndicator(strokeWidth: 2))), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_button.dart b/mobile/lib/widgets/forms/login/login_button.dart deleted file mode 100644 index 0f9fb21d8f..0000000000 --- a/mobile/lib/widgets/forms/login/login_button.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class LoginButton extends ConsumerWidget { - final Function() onPressed; - - const LoginButton({super.key, required this.onPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton.icon( - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), - onPressed: onPressed, - icon: const Icon(Icons.login_rounded), - label: const Text("login", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 2aa770f104..fb3b9c5977 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -17,7 +17,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -34,7 +33,6 @@ import 'package:immich_ui/immich_ui.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:permission_handler/permission_handler.dart'; class LoginForm extends HookConsumerWidget { LoginForm({super.key}); @@ -246,18 +244,14 @@ class LoginForm extends HookConsumerWidget { if (result.shouldChangePassword && !result.isAdmin) { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { - final isBeta = Store.isBetaTimelineEnabled; - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - ref.read(websocketProvider.notifier).connect(); - unawaited(context.replaceRoute(const TabShellRoute())); - return; + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + ref.read(websocketProvider.notifier).connect(); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error) { ImmichToast.show( @@ -338,21 +332,13 @@ class LoginForm extends HookConsumerWidget { .saveAuthInfo(accessToken: loginResponseDto.accessToken); if (isSuccess) { - final permission = ref.watch(galleryPermissionNotifier); - final isBeta = Store.isBetaTimelineEnabled; - if (!isBeta && (permission.isGranted || permission.isLimited)) { - unawaited(ref.watch(backupProvider.notifier).resumeBackup()); + await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); + if (isSyncRemoteDeletionsMode()) { + await getManageMediaPermission(); } - if (isBeta) { - await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { - await getManageMediaPermission(); - } - unawaited(handleSyncFlow()); - unawaited(context.replaceRoute(const TabShellRoute())); - return; - } - unawaited(context.replaceRoute(const TabControllerRoute())); + unawaited(handleSyncFlow()); + unawaited(context.replaceRoute(const TabShellRoute())); + return; } } catch (error, stack) { log.severe('Error logging in with OAuth: $error', stack); diff --git a/mobile/lib/widgets/map/map_app_bar.dart b/mobile/lib/widgets/map/map_app_bar.dart deleted file mode 100644 index 73706c7661..0000000000 --- a/mobile/lib/widgets/map/map_app_bar.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/widgets/map/map_settings_sheet.dart'; - -class MapAppBar extends HookWidget implements PreferredSizeWidget { - final ValueNotifier> selectedAssets; - - const MapAppBar({super.key, required this.selectedAssets}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: context.padding.top + 25), - child: ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (ctx, value, child) => - value.isNotEmpty ? _SelectionRow(selectedAssets: selectedAssets) : const _NonSelectionRow(), - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(100); -} - -class _NonSelectionRow extends StatelessWidget { - const _NonSelectionRow(); - - @override - Widget build(BuildContext context) { - void onSettingsPressed() { - showModalBottomSheet( - elevation: 0.0, - showDragHandle: true, - isScrollControlled: true, - context: context, - builder: (_) => const MapSettingsSheet(), - ); - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () => context.maybePop(), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.arrow_back_ios_new_rounded), - ), - ElevatedButton( - onPressed: onSettingsPressed, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.more_vert_rounded), - ), - ], - ); - } -} - -class _SelectionRow extends HookConsumerWidget { - final ValueNotifier> selectedAssets; - - const _SelectionRow({required this.selectedAssets}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isProcessing = useProcessingOverlay(); - - Future handleProcessing(FutureOr Function() action, [bool reloadMarkers = false]) async { - isProcessing.value = true; - await action(); - // Reset state - selectedAssets.value = {}; - isProcessing.value = false; - if (reloadMarkers) { - ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(true); - } - } - - return Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 20), - child: ElevatedButton.icon( - onPressed: () => selectedAssets.value = {}, - icon: const Icon(Icons.close_rounded), - label: Text( - '${selectedAssets.value.length}', - style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onPrimary), - ), - ), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () => handleProcessing(() => handleShareAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.ios_share_rounded), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleFavoriteAssets(ref, context, selectedAssets.value.toList())), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.favorite), - ), - ElevatedButton( - onPressed: () => - handleProcessing(() => handleArchiveAssets(ref, context, selectedAssets.value.toList()), true), - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.archive), - ), - ], - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart deleted file mode 100644 index b6c1e708a7..0000000000 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/timeline.provider.dart'; -import 'package:immich_mobile/utils/color_filter_generator.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/widgets/common/drag_sheet.dart'; -import 'package:logging/logging.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class MapAssetGrid extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - final ScrollController controller; - - const MapAssetGrid({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - required this.selectedAssets, - required this.controller, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final log = Logger("MapAssetGrid"); - final assetsInBounds = useState>([]); - final cachedRenderList = useRef(null); - final lastRenderElementIndex = useRef(null); - final assetInSheet = useValueNotifier(null); - final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); - - // Add a cache for assets we've already loaded - final assetCache = useRef>({}); - - void handleMapEvents(MapEvent event) async { - if (event is MapAssetsInBoundsUpdated) { - final assetIds = event.assetRemoteIds; - final missingIds = []; - final currentAssets = []; - - for (final id in assetIds) { - final asset = assetCache.value[id]; - if (asset != null) { - currentAssets.add(asset); - } else { - missingIds.add(id); - } - } - - // Only fetch missing assets - if (missingIds.isNotEmpty) { - final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); - - // Add new assets to cache and current list - for (final asset in newAssets) { - if (asset.remoteId != null) { - assetCache.value[asset.remoteId!] = asset; - currentAssets.add(asset); - } - } - } - - assetsInBounds.value = currentAssets; - return; - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - // Hard-restrict to 4 assets / row in portrait mode - const assetsPerRow = 4; - - void handleVisibleItems(Iterable positions) { - final orderedPos = positions.sortedByField((p) => p.index); - // Index of row where the items are mostly visible - const partialOffset = 0.20; - final item = orderedPos.firstWhereOrNull((p) => p.itemTrailingEdge > partialOffset); - - // Guard no elements, reset state - // Also fail fast when the sheet is just opened and the user is yet to scroll (i.e leading = 0) - if (item == null || item.itemLeadingEdge == 0) { - lastRenderElementIndex.value = null; - return; - } - - final renderElement = cachedRenderList.value?.elements.elementAtOrNull(item.index); - // Guard no render list or render element - if (renderElement == null) { - return; - } - // Reset index - lastRenderElementIndex.value == item.index; - - // - // | 1 | 2 | 3 | 4 | 5 | 6 | - // - // | 7 | 8 | 9 | - // - // | 10 | - - // Skip through the assets from the previous row - final rowOffset = renderElement.offset; - // Column offset = (total trailingEdge - trailingEdge crossed) / offset for each asset - final totalOffset = item.itemTrailingEdge - item.itemLeadingEdge; - final edgeOffset = - (totalOffset - partialOffset) / - // Round the total count to the next multiple of [assetsPerRow] - ((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor(); - - // trailing should never be above the totalOffset - final columnOffset = (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/ edgeOffset; - final assetOffset = rowOffset + columnOffset; - final selectedAsset = cachedRenderList.value?.allAssets?.elementAtOrNull(assetOffset)?.remoteId; - - if (selectedAsset != null) { - onGridAssetChanged?.call(selectedAsset); - assetInSheet.value = selectedAsset; - } - } - - return Card( - margin: EdgeInsets.zero, - child: Stack( - children: [ - /// The Align and FractionallySizedBox are to prevent the Asset Grid from going behind the - /// _MapSheetDragRegion and thereby displaying content behind the top right and top left curves - Align( - alignment: Alignment.bottomCenter, - child: FractionallySizedBox( - // Place it just below the drag handle - heightFactor: 0.87, - child: assetsInBounds.value.isNotEmpty - ? ref - .watch(assetsTimelineProvider(assetsInBounds.value)) - .when( - data: (renderList) { - // Cache render list here to use it back during visibleItemsListener - cachedRenderList.value = renderList; - return ValueListenableBuilder( - valueListenable: selectedAssets, - builder: (_, value, __) => ImmichAssetGrid( - shrinkWrap: true, - renderList: renderList, - showDragScroll: false, - assetsPerRow: assetsPerRow, - showMultiSelectIndicator: false, - selectionActive: value.isNotEmpty, - listener: onAssetsSelected, - visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)), - ), - ); - }, - error: (error, stackTrace) { - log.warning("Cannot get assets in the current map bounds", error, stackTrace); - return const SizedBox.shrink(); - }, - loading: () => const SizedBox.shrink(), - ) - : const _MapNoAssetsInSheet(), - ), - ), - _MapSheetDragRegion( - controller: controller, - assetsInBoundCount: assetsInBounds.value.length, - assetInSheet: assetInSheet, - onZoomToAsset: onZoomToAsset, - ), - ], - ), - ); - } -} - -class _MapNoAssetsInSheet extends StatelessWidget { - const _MapNoAssetsInSheet(); - - @override - Widget build(BuildContext context) { - const image = Image(height: 150, width: 150, image: AssetImage('assets/lighthouse.png')); - - return Center( - child: ListView( - shrinkWrap: true, - children: [ - context.isDarkTheme - ? const InvertionFilter( - child: SaturationFilter(saturation: -1, child: BrightnessFilter(brightness: -5, child: image)), - ) - : image, - const SizedBox(height: 20), - Center( - child: Text("map_zoom_to_see_photos".tr(), style: context.textTheme.displayLarge?.copyWith(fontSize: 18)), - ), - ], - ), - ); - } -} - -class _MapSheetDragRegion extends StatelessWidget { - final ScrollController controller; - final int assetsInBoundCount; - final ValueNotifier assetInSheet; - final Function(String)? onZoomToAsset; - - const _MapSheetDragRegion({ - required this.controller, - required this.assetsInBoundCount, - required this.assetInSheet, - this.onZoomToAsset, - }); - - @override - Widget build(BuildContext context) { - final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount}); - - return SingleChildScrollView( - controller: controller, - physics: const ClampingScrollPhysics(), - child: Card( - margin: EdgeInsets.zero, - shape: context.isMobile - ? const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)), - ) - : const BeveledRectangleBorder(), - elevation: 0.0, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 15), - const CustomDraggingHandle(), - const SizedBox(height: 15), - Center( - child: Text( - assetsInBoundsText, - style: TextStyle( - fontSize: 20, - color: context.textTheme.displayLarge?.color?.withValues(alpha: 0.75), - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 8), - ], - ), - ValueListenableBuilder( - valueListenable: assetInSheet, - builder: (_, value, __) => Visibility( - visible: value != null, - child: Positioned( - right: 18, - top: 24, - child: IconButton( - icon: Icon(Icons.map_outlined, color: context.textTheme.displayLarge?.color), - iconSize: 24, - tooltip: 'zoom_to_bounds'.tr(), - onPressed: () => onZoomToAsset?.call(value!), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/map_bottom_sheet.dart b/mobile/lib/widgets/map/map_bottom_sheet.dart deleted file mode 100644 index fba9e9a041..0000000000 --- a/mobile/lib/widgets/map/map_bottom_sheet.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; -import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; -import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; - -class MapBottomSheet extends HookConsumerWidget { - final Stream mapEventStream; - final Function(String)? onGridAssetChanged; - final Function(String)? onZoomToAsset; - final Function()? onZoomToLocation; - final Function(bool, Set)? onAssetsSelected; - final ValueNotifier> selectedAssets; - - const MapBottomSheet({ - required this.mapEventStream, - this.onGridAssetChanged, - this.onZoomToAsset, - this.onAssetsSelected, - this.onZoomToLocation, - required this.selectedAssets, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - const sheetMinExtent = 0.1; - final sheetController = useDraggableScrollController(); - final bottomSheetOffset = useValueNotifier(sheetMinExtent); - final isBottomSheetOpened = useRef(false); - - void handleMapEvents(MapEvent event) async { - if (event is MapCloseBottomSheet) { - await sheetController.animateTo( - 0.1, - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - ); - } - } - - useOnStreamChange(mapEventStream, onData: handleMapEvents); - - bool onScrollNotification(DraggableScrollableNotification notification) { - isBottomSheetOpened.value = notification.extent > (notification.maxExtent * 0.9); - bottomSheetOffset.value = notification.extent; - // do not bubble - return true; - } - - return Stack( - children: [ - NotificationListener( - onNotification: onScrollNotification, - child: DraggableScrollableSheet( - controller: sheetController, - minChildSize: sheetMinExtent, - maxChildSize: 0.8, - initialChildSize: sheetMinExtent, - snap: true, - snapSizes: [sheetMinExtent, 0.5, 0.8], - shouldCloseOnMinExtent: false, - builder: (ctx, scrollController) => MapAssetGrid( - controller: scrollController, - mapEventStream: mapEventStream, - selectedAssets: selectedAssets, - onAssetsSelected: onAssetsSelected, - // Do not bother with the event if the bottom sheet is not user scrolled - onGridAssetChanged: (assetId) => isBottomSheetOpened.value ? onGridAssetChanged?.call(assetId) : null, - onZoomToAsset: onZoomToAsset, - ), - ), - ), - ValueListenableBuilder( - valueListenable: bottomSheetOffset, - builder: (context, value, child) { - return Positioned( - right: 0, - bottom: context.height * (value + 0.02), - child: AnimatedOpacity( - opacity: value < 0.8 ? 1 : 0, - duration: const Duration(milliseconds: 150), - child: ElevatedButton( - onPressed: onZoomToLocation, - style: ElevatedButton.styleFrom(shape: const CircleBorder()), - child: const Icon(Icons.my_location), - ), - ), - ); - }, - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/map/map_settings_sheet.dart b/mobile/lib/widgets/map/map_settings_sheet.dart deleted file mode 100644 index 644056d153..0000000000 --- a/mobile/lib/widgets/map/map_settings_sheet.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; -import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; - -class MapSettingsSheet extends HookConsumerWidget { - const MapSettingsSheet({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mapState = ref.watch(mapStateNotifierProvider); - - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.6, - builder: (ctx, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Card( - elevation: 0.0, - shadowColor: Colors.transparent, - margin: EdgeInsets.zero, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - MapThemePicker( - themeMode: mapState.themeMode, - onThemeChange: (mode) => ref.read(mapStateNotifierProvider.notifier).switchTheme(mode), - ), - const Divider(height: 30, thickness: 2), - MapSettingsListTile( - title: "map_settings_only_show_favorites", - selected: mapState.showFavoriteOnly, - onChanged: (favoriteOnly) => - ref.read(mapStateNotifierProvider.notifier).switchFavoriteOnly(favoriteOnly), - ), - MapSettingsListTile( - title: "map_settings_include_show_archived", - selected: mapState.includeArchived, - onChanged: (includeArchive) => - ref.read(mapStateNotifierProvider.notifier).switchIncludeArchived(includeArchive), - ), - MapSettingsListTile( - title: "map_settings_include_show_partners", - selected: mapState.withPartners, - onChanged: (withPartners) => - ref.read(mapStateNotifierProvider.notifier).switchWithPartners(withPartners), - ), - MapTimeDropDown( - relativeTime: mapState.relativeTime, - onTimeChange: (time) => ref.read(mapStateNotifierProvider.notifier).setRelativeTime(time), - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart deleted file mode 100644 index b6d7241cf4..0000000000 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; - -class PositionedAssetMarkerIcon extends StatelessWidget { - final Point point; - final String assetRemoteId; - final String assetThumbhash; - final double size; - final int durationInMilliseconds; - - final Function()? onTap; - - const PositionedAssetMarkerIcon({ - required this.point, - required this.assetRemoteId, - required this.assetThumbhash, - this.size = 100, - this.durationInMilliseconds = 100, - this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio; - return AnimatedPositioned( - left: point.x / ratio - size / 2, - top: point.y / ratio - size, - duration: Duration(milliseconds: durationInMilliseconds), - child: GestureDetector( - onTap: () => onTap?.call(), - child: SizedBox.square( - dimension: size, - child: AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_bottom_info.dart b/mobile/lib/widgets/memories/memory_bottom_info.dart deleted file mode 100644 index 4b43821782..0000000000 --- a/mobile/lib/widgets/memories/memory_bottom_info.dart +++ /dev/null @@ -1,50 +0,0 @@ -// ignore_for_file: require_trailing_commas - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; - -class MemoryBottomInfo extends StatelessWidget { - final Memory memory; - - const MemoryBottomInfo({super.key, required this.memory}); - - @override - Widget build(BuildContext context) { - final df = DateFormat.yMMMMd(); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - memory.title, - style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500), - ), - Text( - df.format(memory.assets[0].fileCreatedAt), - style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500), - ), - ], - ), - MaterialButton( - minWidth: 0, - onPressed: () { - context.maybePop(); - scrollToDateNotifierProvider.scrollToDate(memory.assets[0].fileCreatedAt); - }, - shape: const CircleBorder(), - color: Colors.white.withValues(alpha: 0.2), - elevation: 0, - child: const Icon(Icons.open_in_new, color: Colors.white), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart deleted file mode 100644 index 189cc67428..0000000000 --- a/mobile/lib/widgets/memories/memory_card.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryCard extends StatelessWidget { - final Asset asset; - final String title; - final bool showTitle; - final Function()? onVideoEnded; - - const MemoryCard({required this.asset, required this.title, required this.showTitle, this.onVideoEnded, super.key}); - - @override - Widget build(BuildContext context) { - return Card( - color: Colors.black, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - side: BorderSide(color: Colors.black, width: 1.0), - ), - clipBehavior: Clip.hardEdge, - child: Stack( - children: [ - SizedBox.expand(child: _BlurredBackdrop(asset: asset)), - LayoutBuilder( - builder: (context, constraints) { - // Determine the fit using the aspect ratio - BoxFit fit = BoxFit.contain; - if (asset.width != null && asset.height != null) { - final aspectRatio = asset.width! / asset.height!; - final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight; - // Look for a 25% difference in either direction - if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) { - // Cover to look nice if we have nearly the same aspect ratio - fit = BoxFit.cover; - } - } - - if (asset.isImage) { - return Hero( - tag: 'memory-${asset.id}', - child: ImmichImage(asset, fit: fit, height: double.infinity, width: double.infinity), - ); - } else { - return Hero( - tag: 'memory-${asset.id}', - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewerPage( - key: ValueKey(asset.id), - asset: asset, - showControls: false, - playbackDelayFactor: 2, - image: ImmichImage(asset, width: context.width, height: context.height, fit: BoxFit.contain), - ), - ), - ); - } - }, - ), - if (showTitle) - Positioned( - left: 18.0, - bottom: 18.0, - child: Text( - title, - style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ); - } -} - -class _BlurredBackdrop extends HookWidget { - final Asset asset; - - const _BlurredBackdrop({required this.asset}); - - @override - Widget build(BuildContext context) { - final blurhash = useBlurHashRef(asset).value; - if (blurhash != null) { - // Use a nice cheap blur hash image decoration - return Container( - decoration: BoxDecoration( - image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ); - } else { - // Fall back to using a more expensive image filtered - // Since the ImmichImage is already precached, we can - // safely use that as the image provider - return ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: ImmichImage.imageProvider(asset: asset, height: context.height, width: context.width), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withValues(alpha: 0.2)), - ), - ); - } - } -} diff --git a/mobile/lib/widgets/memories/memory_lane.dart b/mobile/lib/widgets/memories/memory_lane.dart deleted file mode 100644 index 4cba83bea7..0000000000 --- a/mobile/lib/widgets/memories/memory_lane.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/memories/memory.model.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_image.dart'; - -class MemoryLane extends HookConsumerWidget { - const MemoryLane({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final memoryLaneFutureProvider = ref.watch(memoryFutureProvider); - - final memoryLane = memoryLaneFutureProvider - .whenData( - (memories) => memories != null - ? ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: CarouselView( - itemExtent: 145.0, - shrinkExtent: 1.0, - elevation: 2, - backgroundColor: Colors.black, - overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)), - onTap: (memoryIndex) { - ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (memories[memoryIndex].assets.isNotEmpty) { - final asset = memories[memoryIndex].assets[0]; - ref.read(currentAssetProvider.notifier).set(asset); - } - context.pushRoute(MemoryRoute(memories: memories, memoryIndex: memoryIndex)); - }, - children: memories - .mapIndexed((index, memory) => MemoryCard(index: index, memory: memory)) - .toList(), - ), - ) - : const SizedBox(), - ) - .value; - - return memoryLane ?? const SizedBox(); - } -} - -class MemoryCard extends ConsumerWidget { - const MemoryCard({super.key, required this.index, required this.memory}); - - final int index; - final Memory memory; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Center( - child: Stack( - children: [ - ColorFiltered( - colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: ImmichImage( - memory.assets[0], - fit: BoxFit.cover, - width: 205, - height: 200, - placeholder: const ThumbnailPlaceholder(width: 105, height: 200), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 114), - child: Text( - memory.title, - style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15), - ), - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/search/curated_people_row.dart b/mobile/lib/widgets/search/curated_people_row.dart deleted file mode 100644 index 9155de2131..0000000000 --- a/mobile/lib/widgets/search/curated_people_row.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; - -class CuratedPeopleRow extends StatelessWidget { - static const double imageSize = 60.0; - - final List content; - final EdgeInsets? padding; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - final Function(SearchCuratedContent, int)? onNameTap; - - const CuratedPeopleRow({super.key, required this.content, this.onTap, this.padding, required this.onNameTap}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: SingleChildScrollView( - padding: padding, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate(content.length, (index) { - final person = content[index]; - return Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - onTap: () => onTap?.call(person, index), - child: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), - ), - ), - ), - ), - const SizedBox(height: 8), - SizedBox(width: imageSize, child: _buildPersonLabel(context, person, index)), - ], - ), - ); - }), - ), - ), - ); - } - - Widget _buildPersonLabel(BuildContext context, SearchCuratedContent person, int index) { - if (person.label.isEmpty) { - return GestureDetector( - onTap: () => onNameTap?.call(person, index), - child: Text( - "exif_bottom_sheet_person_add_person", - style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ).tr(), - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - person.label, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: context.textTheme.labelLarge, - maxLines: 2, - ), - if (person.subtitle != null) Text(person.subtitle!, textAlign: TextAlign.center), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/curated_places_row.dart b/mobile/lib/widgets/search/curated_places_row.dart deleted file mode 100644 index 9d21292bde..0000000000 --- a/mobile/lib/widgets/search/curated_places_row.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/widgets/search/search_map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class CuratedPlacesRow extends StatelessWidget { - const CuratedPlacesRow({ - super.key, - required this.content, - required this.imageSize, - this.isMapEnabled = true, - this.onTap, - }); - - final bool isMapEnabled; - final List content; - final double imageSize; - - /// Callback with the content and the index when tapped - final Function(SearchCuratedContent, int)? onTap; - - @override - Widget build(BuildContext context) { - // Calculating the actual index of the content based on the whether map is enabled or not. - // If enabled, inject map as the first item in the list (index 0) and so the actual content will start from index 1 - final int actualContentIndex = isMapEnabled ? 1 : 0; - - return SizedBox( - height: imageSize, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - separatorBuilder: (context, index) => const SizedBox(width: 10), - itemBuilder: (context, index) { - // Injecting Map thumbnail as the first element - if (isMapEnabled && index == 0) { - return SizedBox.square( - dimension: imageSize, - child: SearchMapThumbnail(size: imageSize), - ); - } - final actualIndex = index - actualContentIndex; - final object = content[actualIndex]; - final thumbnailRequestUrl = '${Store.get(StoreKey.serverEndpoint)}/assets/${object.id}/thumbnail'; - return SizedBox.square( - dimension: imageSize, - child: ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: object.label, - onTap: () => onTap?.call(object, actualIndex), - ), - ); - }, - itemCount: content.length + actualContentIndex, - ), - ); - } -} diff --git a/mobile/lib/widgets/search/explore_grid.dart b/mobile/lib/widgets/search/explore_grid.dart deleted file mode 100644 index 6af20df029..0000000000 --- a/mobile/lib/widgets/search/explore_grid.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; - -class ExploreGrid extends StatelessWidget { - final List curatedContent; - final bool isPeople; - - const ExploreGrid({super.key, required this.curatedContent, this.isPeople = false}); - - @override - Widget build(BuildContext context) { - if (curatedContent.isEmpty) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - height: 100, - width: 100, - child: ThumbnailWithInfo(textInfo: '', onTap: () {}), - ), - ); - } - - return GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 140, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - ), - itemBuilder: (context, index) { - final content = curatedContent[index]; - final thumbnailRequestUrl = isPeople - ? getFaceThumbnailUrl(content.id) - : '${Store.get(StoreKey.serverEndpoint)}/assets/${content.id}/thumbnail'; - - return ThumbnailWithInfo( - imageUrl: thumbnailRequestUrl, - textInfo: content.label, - borderRadius: 0, - onTap: () { - isPeople - ? context.pushRoute(PersonResultRoute(personId: content.id, personName: content.label)) - : context.pushRoute( - SearchRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter(city: content.label), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ), - ); - }, - ); - }, - itemCount: curatedContent.length, - ); - } -} diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart deleted file mode 100644 index 3fa443121a..0000000000 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; - -class PersonNameEditFormResult { - final bool success; - final String updatedName; - - const PersonNameEditFormResult(this.success, this.updatedName); -} - -class PersonNameEditForm extends HookConsumerWidget { - final String personId; - final String personName; - - const PersonNameEditForm({super.key, required this.personId, required this.personName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final controller = useTextEditingController(text: personName); - final isError = useState(false); - - return AlertDialog( - title: const Text("add_a_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - content: SingleChildScrollView( - child: TextFormField( - controller: controller, - textCapitalization: TextCapitalization.words, - autofocus: true, - decoration: InputDecoration( - hintText: 'name'.tr(), - border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occurred' : null, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => context.pop(const PersonNameEditFormResult(false, '')), - child: Text( - "cancel", - style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), - ), - TextButton( - onPressed: () async { - isError.value = false; - final result = await ref.read(updatePersonNameProvider(personId, controller.text).future); - isError.value = !result; - if (result) { - context.pop(PersonNameEditFormResult(true, controller.text)); - } - }, - child: Text( - "save", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/search/search_filter/media_type_picker.dart b/mobile/lib/widgets/search/search_filter/media_type_picker.dart index e0e34b654e..ac89de8190 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class MediaTypePicker extends HookWidget { const MediaTypePicker({super.key, required this.onSelect, this.filter}); diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 978b70239c..a7b0286df3 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -57,6 +57,7 @@ class PeoplePicker extends HookConsumerWidget { final isSelected = selectedPeople.value.contains(person); return Padding( + key: ValueKey(person.id), padding: const EdgeInsets.only(bottom: 2.0), child: LargeLeadingTile( title: Text( @@ -73,6 +74,7 @@ class PeoplePicker extends HookConsumerWidget { shape: const CircleBorder(side: BorderSide.none), elevation: 3, child: CircleAvatar( + key: ValueKey(person.id), maxRadius: imageSize / 2, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), ), diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart deleted file mode 100644 index 7533e46f1a..0000000000 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -class SearchMapThumbnail extends StatelessWidget { - const SearchMapThumbnail({super.key, this.size = 60.0}); - - final double size; - final bool showTitle = true; - - @override - Widget build(BuildContext context) { - return ThumbnailWithInfoContainer( - label: 'search_page_your_map'.tr(), - onTap: () { - context.pushRoute(MapRoute()); - }, - child: IgnorePointer( - child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false), - ), - ); - } -} diff --git a/mobile/lib/widgets/search/search_row_section.dart b/mobile/lib/widgets/search/search_row_section.dart deleted file mode 100644 index b8584fefef..0000000000 --- a/mobile/lib/widgets/search/search_row_section.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/widgets/search/search_row_title.dart'; - -class SearchRowSection extends StatelessWidget { - const SearchRowSection({ - super.key, - required this.onViewAllPressed, - required this.title, - this.isEmpty = false, - required this.child, - }); - - final Function() onViewAllPressed; - final String title; - final bool isEmpty; - final Widget child; - - @override - Widget build(BuildContext context) { - if (isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SearchRowTitle(onViewAllPressed: onViewAllPressed, title: title), - ), - child, - ], - ); - } -} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index d5905a246c..a38ccd3556 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -14,9 +13,7 @@ import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; -import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -35,7 +32,6 @@ class AdvancedSettings extends HookConsumerWidget { final manageMediaAndroidPermission = useState(false); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); - final useAlternatePMFilter = useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; @@ -114,35 +110,26 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_prefer_remote_title".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(), ), - if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(), const CustomProxyHeaderSettings(), const SslClientCertSettings(), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useAlternatePMFilter, - title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), - ), - if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(), - if (Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: readonlyModeEnabled, - title: "advanced_settings_readonly_mode_title".tr(), - subtitle: "advanced_settings_readonly_mode_subtitle".tr(), - onChanged: (value) { - readonlyModeEnabled.value = value; - ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); - context.scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), - ), + SettingsSwitchListTile( + valueNotifier: readonlyModeEnabled, + title: "advanced_settings_readonly_mode_title".tr(), + subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + onChanged: (value) { + readonlyModeEnabled.value = value; + ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), - ); - }, - ), + ), + ); + }, + ), ListTile( title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 08e66df48d..42ea3acfc0 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 2d5c9f06eb..55c8195947 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,21 +1,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class LayoutSettings extends HookConsumerWidget { const LayoutSettings({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout); final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow); return Column( @@ -25,12 +22,6 @@ class LayoutSettings extends HookConsumerWidget { title: "asset_list_layout_sub_title".t(context: context), icon: Icons.view_module_outlined, ), - if (!Store.isBetaTimelineEnabled) - SettingsSwitchListTile( - valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), - ), SettingsSliderListTile( valueNotifier: tilesPerRow, text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), diff --git a/mobile/lib/widgets/settings/backup_settings/background_settings.dart b/mobile/lib/widgets/settings/backup_settings/background_settings.dart deleted file mode 100644 index 038a567dc2..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/background_settings.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:immich_mobile/widgets/backup/ios_debug_info_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BackgroundBackupSettings extends ConsumerWidget { - const BackgroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isBackgroundEnabled = ref.watch(backupProvider.select((s) => s.backgroundBackup)); - final iosSettings = ref.watch(iOSBackgroundSettingsProvider); - - void showErrorToUser(String msg) { - final snackBar = SnackBar( - content: Text(msg.tr(), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor)), - backgroundColor: Colors.red, - ); - context.scaffoldMessenger.showSnackBar(snackBar); - } - - void showBatteryOptimizationInfoToUser() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext ctx) { - return AlertDialog( - title: const Text('backup_controller_page_background_battery_info_title').tr(), - content: SingleChildScrollView( - child: const Text('backup_controller_page_background_battery_info_message').tr(), - ), - actions: [ - ElevatedButton( - onPressed: () => - launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), - child: const Text( - "backup_controller_page_background_battery_info_link", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - ), - ElevatedButton( - child: const Text( - 'backup_controller_page_background_battery_info_ok', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - onPressed: () => ctx.pop(), - ), - ], - ); - }, - ); - } - - if (!isBackgroundEnabled) { - return SettingsButtonListTile( - icon: Icons.cloud_sync_outlined, - title: 'backup_controller_page_background_is_off'.tr(), - subtileText: 'backup_controller_page_background_description'.tr(), - buttonText: 'backup_controller_page_background_turn_on'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - enabled: true, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ); - } - - return Column( - children: [ - if (!Platform.isIOS || iosSettings?.appRefreshEnabled == true) - _BackgroundSettingsEnabled(onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser), - if (Platform.isIOS && iosSettings?.appRefreshEnabled != true) const _IOSBackgroundRefreshDisabled(), - if (Platform.isIOS && iosSettings != null) IosDebugInfoTile(settings: iosSettings), - ], - ); - } -} - -class _IOSBackgroundRefreshDisabled extends StatelessWidget { - const _IOSBackgroundRefreshDisabled(); - - @override - Widget build(BuildContext context) { - return SettingsButtonListTile( - icon: Icons.task_outlined, - title: 'backup_controller_page_background_app_refresh_disabled_title'.tr(), - subtileText: 'backup_controller_page_background_app_refresh_disabled_content'.tr(), - buttonText: 'backup_controller_page_background_app_refresh_enable_button_text'.tr(), - onButtonTap: () => openAppSettings(), - ); - } -} - -class _BackgroundSettingsEnabled extends HookConsumerWidget { - final void Function(String msg) onError; - final void Function() onBatteryInfo; - - const _BackgroundSettingsEnabled({required this.onError, required this.onBatteryInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isWifiRequired = ref.watch(backupProvider.select((s) => s.backupRequireWifi)); - final isWifiRequiredNotifier = useValueNotifier(isWifiRequired); - useValueChanged( - isWifiRequired, - (_, __) => WidgetsBinding.instance.addPostFrameCallback((_) => isWifiRequiredNotifier.value = isWifiRequired), - ); - - final isChargingRequired = ref.watch(backupProvider.select((s) => s.backupRequireCharging)); - final isChargingRequiredNotifier = useValueNotifier(isChargingRequired); - useValueChanged( - isChargingRequired, - (_, __) => - WidgetsBinding.instance.addPostFrameCallback((_) => isChargingRequiredNotifier.value = isChargingRequired), - ); - - int backupDelayToSliderValue(int ms) => switch (ms) { - 5000 => 0, - 30000 => 1, - 120000 => 2, - _ => 3, - }; - - int backupDelayToMilliseconds(int v) => switch (v) { - 0 => 5000, - 1 => 30000, - 2 => 120000, - _ => 600000, - }; - - String formatBackupDelaySliderValue(int v) => switch (v) { - 0 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '5'}), - 1 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '30'}), - 2 => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '2'}), - _ => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '10'}), - }; - - final backupTriggerDelay = ref.watch(backupProvider.select((s) => s.backupTriggerDelay)); - final triggerDelay = useState(backupDelayToSliderValue(backupTriggerDelay)); - useValueChanged( - triggerDelay.value, - (_, __) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - triggerDelay: backupDelayToMilliseconds(triggerDelay.value), - onError: onError, - onBatteryInfo: onBatteryInfo, - ), - ); - - return SettingsButtonListTile( - icon: Icons.cloud_sync_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_background_is_on'.tr(), - buttonText: 'backup_controller_page_background_turn_off'.tr(), - onButtonTap: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(enabled: false, onError: onError, onBatteryInfo: onBatteryInfo), - subtitle: Column( - children: [ - SettingsSwitchListTile( - valueNotifier: isWifiRequiredNotifier, - title: 'backup_controller_page_background_wifi'.tr(), - icon: Icons.wifi, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireWifi: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - SettingsSwitchListTile( - valueNotifier: isChargingRequiredNotifier, - title: 'backup_controller_page_background_charging'.tr(), - icon: Icons.charging_station, - onChanged: (enabled) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup(requireCharging: enabled, onError: onError, onBatteryInfo: onBatteryInfo), - ), - if (Platform.isAndroid) - SettingsSliderListTile( - valueNotifier: triggerDelay, - text: 'backup_controller_page_background_delay'.tr( - namedArgs: {'duration': formatBackupDelaySliderValue(triggerDelay.value)}, - ), - maxValue: 3.0, - noDivisons: 3, - label: formatBackupDelaySliderValue(triggerDelay.value), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart deleted file mode 100644 index 50aa57da9f..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/backup/backup_verification.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; -import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; - -class BackupSettings extends HookConsumerWidget { - const BackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ignoreIcloudAssets = useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); - final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); - final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); - final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); - final isAlbumSyncInProgress = useState(false); - - syncAlbums() async { - isAlbumSyncInProgress.value = true; - try { - await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); - } catch (_) { - } finally { - Future.delayed(const Duration(seconds: 1), () { - isAlbumSyncInProgress.value = false; - }); - } - } - - final backupSettings = [ - const ForegroundBackupSettings(), - const BackgroundBackupSettings(), - if (Platform.isIOS) - SettingsSwitchListTile( - valueNotifier: ignoreIcloudAssets, - title: 'ignore_icloud_photos'.tr(), - subtitle: 'ignore_icloud_photos_description'.tr(), - ), - if (Platform.isAndroid && isAdvancedTroubleshooting.value) - SettingsButtonListTile( - icon: Icons.warning_rounded, - title: 'check_corrupt_asset_backup'.tr(), - subtitle: isCorruptCheckInProgress - ? const Column( - children: [ - SizedBox(height: 20), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), - ], - ) - : null, - subtileText: !isCorruptCheckInProgress ? 'check_corrupt_asset_backup_description'.tr() : null, - buttonText: 'check_corrupt_asset_backup_button'.tr(), - onButtonTap: !isCorruptCheckInProgress - ? () => ref.read(backupVerificationProvider.notifier).performBackupCheck(context) - : null, - ), - if (albumSync.value) - SettingsButtonListTile( - icon: Icons.photo_album_outlined, - title: 'sync_albums'.tr(), - subtitle: Text("sync_albums_manual_subtitle".tr()), - buttonText: 'sync_albums'.tr(), - child: isAlbumSyncInProgress.value - ? const CircularProgressIndicator() - : ElevatedButton(onPressed: syncAlbums, child: Text('sync'.tr())), - ), - ]; - - return SettingsSubPageScaffold(settings: backupSettings, showDivider: true); - } -} diff --git a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart b/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart deleted file mode 100644 index a2ff00fe45..0000000000 --- a/mobile/lib/widgets/settings/backup_settings/foreground_settings.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; -import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; - -class ForegroundBackupSettings extends ConsumerWidget { - const ForegroundBackupSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isAutoBackup = ref.watch(backupProvider.select((s) => s.autoBackup)); - - void onButtonTap() => ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); - - if (isAutoBackup) { - return SettingsButtonListTile( - icon: Icons.cloud_done_rounded, - iconColor: context.primaryColor, - title: 'backup_controller_page_status_on'.tr(), - buttonText: 'backup_controller_page_turn_off'.tr(), - onButtonTap: onButtonTap, - ); - } - - return SettingsButtonListTile( - icon: Icons.cloud_off_rounded, - title: 'backup_controller_page_status_off'.tr(), - subtileText: 'backup_controller_page_desc_backup'.tr(), - buttonText: 'backup_controller_page_turn_on'.tr(), - onButtonTap: onButtonTap, - ); - } -} diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart deleted file mode 100644 index 21e0edb34c..0000000000 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; - -class BetaTimelineListTile extends ConsumerWidget { - const BetaTimelineListTile({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); - final auth = ref.watch(authProvider); - - if (!auth.isAuthenticated) { - return const SizedBox.shrink(); - } - - void onSwitchChanged(bool value) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: value ? const Text("Enable New Timeline") : const Text("Disable New Timeline"), - content: value - ? const Text("Are you sure you want to enable the new timeline?") - : const Text("Are you sure you want to disable the new timeline?"), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: Text( - "cancel".t(context: context), - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), - ), - ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)])); - }, - child: Text("ok".t(context: context)), - ), - ], - ); - }, - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 4.0), - child: SettingListTile( - title: "new_timeline".t(context: context), - trailing: Switch.adaptive( - value: betaTimelineValue, - onChanged: onSwitchChanged, - activeThumbColor: context.primaryColor, - ), - onTap: () => onSwitchChanged(!betaTimelineValue), - ), - ); - } -} diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart deleted file mode 100644 index af9e4079bb..0000000000 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; - -class LocalStorageSettings extends HookConsumerWidget { - const LocalStorageSettings({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isarDb = ref.watch(dbProvider); - final cacheItemCount = useState(0); - - useEffect(() { - cacheItemCount.value = isarDb.duplicatedAssets.countSync(); - return null; - }, []); - - void clearCache() async { - await isarDb.writeTxn(() => isarDb.duplicatedAssets.clear()); - cacheItemCount.value = await isarDb.duplicatedAssets.count(); - } - - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - dense: true, - title: Text( - "cache_settings_duplicated_assets_title", - style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), - ).tr(namedArgs: {'count': "${cacheItemCount.value}"}), - subtitle: Text( - "cache_settings_duplicated_assets_subtitle", - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), - trailing: TextButton( - onPressed: cacheItemCount.value > 0 ? clearCache : null, - child: Text( - "cache_settings_duplicated_assets_clear_button", - style: TextStyle( - fontSize: 12, - color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ); - } -} diff --git a/mobile/mise.toml b/mobile/mise.toml index 88b8902053..4d20a0a149 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,5 +1,5 @@ [tools] -flutter = "3.35.7" +flutter = "3.41.6" [tools."github:CQLabs/homebrew-dcm"] version = "1.30.0" @@ -40,7 +40,13 @@ depends = [ [tasks."codegen:translation"] alias = "translation" description = "Generate translations from i18n JSONs" -run = [{ task = "//:i18n:format-fix" }, { tasks = ["i18n:loader", "i18n:keys"] }] +run = [ + { task = "//:i18n:format-fix" }, + { tasks = [ + "i18n:loader", + "i18n:keys", + ] }, +] [tasks."codegen:app-icon"] description = "Generate app icons" diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 39d9ce2510..138cf1735a 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 2.7.4 +- API version: 2.7.5 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -56,10 +56,10 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = APIKeysApi(); -final aPIKeyCreateDto = APIKeyCreateDto(); // APIKeyCreateDto | +final apiKeyCreateDto = ApiKeyCreateDto(); // ApiKeyCreateDto | try { - final result = api_instance.createApiKey(aPIKeyCreateDto); + final result = api_instance.createApiKey(apiKeyCreateDto); print(result); } catch (e) { print('Exception when calling APIKeysApi->createApiKey: $e\n'); @@ -89,6 +89,7 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | Create an album *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | Delete an album *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | Retrieve an album +*AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers *AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album @@ -96,24 +97,20 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload -*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Check existing assets *AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset *AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets *AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset *AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset -*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key *AssetsApi* | [**getAssetOcr**](doc//AssetsApi.md#getassetocr) | **GET** /assets/{id}/ocr | Retrieve asset OCR data *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics -*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video *AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset -*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset *AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata | Update asset metadata @@ -144,12 +141,7 @@ Class | Method | HTTP request | Description *DatabaseBackupsAdminApi* | [**startDatabaseRestoreFlow**](doc//DatabaseBackupsAdminApi.md#startdatabaserestoreflow) | **POST** /admin/database-backups/start-restore | Start database backup restore flow *DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup *DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner -*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID -*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status -*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets -*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information @@ -239,7 +231,6 @@ Class | Method | HTTP request | Description *ServerApi* | [**getServerVersion**](doc//ServerApi.md#getserverversion) | **GET** /server/version | Get server version *ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage | Get storage *ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types | Get supported media types -*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme | Get theme *ServerApi* | [**getVersionCheck**](doc//ServerApi.md#getversioncheck) | **GET** /server/version-check | Get version check status *ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history | Get version history *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping | Ping @@ -267,8 +258,6 @@ Class | Method | HTTP request | Description *StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks | Retrieve stacks *StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} | Update a stack *SyncApi* | [**deleteSyncAck**](doc//SyncApi.md#deletesyncack) | **DELETE** /sync/ack | Delete acknowledgements -*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user -*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *SyncApi* | [**getSyncAck**](doc//SyncApi.md#getsyncack) | **GET** /sync/ack | Retrieve acknowledgements *SyncApi* | [**getSyncStream**](doc//SyncApi.md#getsyncstream) | **POST** /sync/stream | Stream sync changes *SyncApi* | [**sendSyncAck**](doc//SyncApi.md#sendsyncack) | **POST** /sync/ack | Acknowledge changes @@ -330,10 +319,6 @@ Class | Method | HTTP request | Description ## Documentation For Models - - [APIKeyCreateDto](doc//APIKeyCreateDto.md) - - [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md) - - [APIKeyResponseDto](doc//APIKeyResponseDto.md) - - [APIKeyUpdateDto](doc//APIKeyUpdateDto.md) - [ActivityCreateDto](doc//ActivityCreateDto.md) - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) @@ -349,6 +334,10 @@ Class | Method | HTTP request | Description - [AlbumsAddAssetsResponseDto](doc//AlbumsAddAssetsResponseDto.md) - [AlbumsResponse](doc//AlbumsResponse.md) - [AlbumsUpdate](doc//AlbumsUpdate.md) + - [ApiKeyCreateDto](doc//ApiKeyCreateDto.md) + - [ApiKeyCreateResponseDto](doc//ApiKeyCreateResponseDto.md) + - [ApiKeyResponseDto](doc//ApiKeyResponseDto.md) + - [ApiKeyUpdateDto](doc//ApiKeyUpdateDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) - [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md) @@ -356,8 +345,6 @@ Class | Method | HTTP request | Description - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) - [AssetCopyDto](doc//AssetCopyDto.md) - - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) - [AssetEditAction](doc//AssetEditAction.md) - [AssetEditActionItemDto](doc//AssetEditActionItemDto.md) - [AssetEditActionItemDtoParameters](doc//AssetEditActionItemDtoParameters.md) @@ -370,7 +357,7 @@ Class | Method | HTTP request | Description - [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md) - [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md) - [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md) - - [AssetFullSyncDto](doc//AssetFullSyncDto.md) + - [AssetIdErrorReason](doc//AssetIdErrorReason.md) - [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetJobName](doc//AssetJobName.md) @@ -388,10 +375,12 @@ Class | Method | HTTP request | Description - [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md) - [AssetOcrResponseDto](doc//AssetOcrResponseDto.md) - [AssetOrder](doc//AssetOrder.md) + - [AssetRejectReason](doc//AssetRejectReason.md) - [AssetResponseDto](doc//AssetResponseDto.md) - [AssetStackResponseDto](doc//AssetStackResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) + - [AssetUploadAction](doc//AssetUploadAction.md) - [AssetVisibility](doc//AssetVisibility.md) - [AudioCodec](doc//AudioCodec.md) - [AuthStatusResponseDto](doc//AuthStatusResponseDto.md) @@ -404,8 +393,6 @@ Class | Method | HTTP request | Description - [CastResponse](doc//CastResponse.md) - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [Colorspace](doc//Colorspace.md) - [ContributorCountResponseDto](doc//ContributorCountResponseDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) @@ -440,7 +427,6 @@ Class | Method | HTTP request | Description - [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LicenseKeyDto](doc//LicenseKeyDto.md) - - [LicenseResponseDto](doc//LicenseResponseDto.md) - [LogLevel](doc//LogLevel.md) - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) @@ -504,6 +490,10 @@ Class | Method | HTTP request | Description - [PluginActionResponseDto](doc//PluginActionResponseDto.md) - [PluginContextType](doc//PluginContextType.md) - [PluginFilterResponseDto](doc//PluginFilterResponseDto.md) + - [PluginJsonSchema](doc//PluginJsonSchema.md) + - [PluginJsonSchemaProperty](doc//PluginJsonSchemaProperty.md) + - [PluginJsonSchemaPropertyAdditionalProperties](doc//PluginJsonSchemaPropertyAdditionalProperties.md) + - [PluginJsonSchemaType](doc//PluginJsonSchemaType.md) - [PluginResponseDto](doc//PluginResponseDto.md) - [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md) - [PluginTriggerType](doc//PluginTriggerType.md) @@ -545,7 +535,6 @@ Class | Method | HTTP request | Description - [ServerPingResponse](doc//ServerPingResponse.md) - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerStorageResponseDto](doc//ServerStorageResponseDto.md) - - [ServerThemeDto](doc//ServerThemeDto.md) - [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) - [SessionCreateDto](doc//SessionCreateDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 6b554fb644..9eca7a2ab7 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -68,10 +68,6 @@ part 'api/users_admin_api.dart'; part 'api/views_api.dart'; part 'api/workflows_api.dart'; -part 'model/api_key_create_dto.dart'; -part 'model/api_key_create_response_dto.dart'; -part 'model/api_key_response_dto.dart'; -part 'model/api_key_update_dto.dart'; part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; @@ -87,6 +83,10 @@ part 'model/albums_add_assets_dto.dart'; part 'model/albums_add_assets_response_dto.dart'; part 'model/albums_response.dart'; part 'model/albums_update.dart'; +part 'model/api_key_create_dto.dart'; +part 'model/api_key_create_response_dto.dart'; +part 'model/api_key_response_dto.dart'; +part 'model/api_key_update_dto.dart'; part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_update_dto.dart'; part 'model/asset_bulk_upload_check_dto.dart'; @@ -94,8 +94,6 @@ part 'model/asset_bulk_upload_check_item.dart'; part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_copy_dto.dart'; -part 'model/asset_delta_sync_dto.dart'; -part 'model/asset_delta_sync_response_dto.dart'; part 'model/asset_edit_action.dart'; part 'model/asset_edit_action_item_dto.dart'; part 'model/asset_edit_action_item_dto_parameters.dart'; @@ -108,7 +106,7 @@ part 'model/asset_face_response_dto.dart'; part 'model/asset_face_update_dto.dart'; part 'model/asset_face_update_item.dart'; part 'model/asset_face_without_person_response_dto.dart'; -part 'model/asset_full_sync_dto.dart'; +part 'model/asset_id_error_reason.dart'; part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; part 'model/asset_job_name.dart'; @@ -126,10 +124,12 @@ part 'model/asset_metadata_upsert_dto.dart'; part 'model/asset_metadata_upsert_item_dto.dart'; part 'model/asset_ocr_response_dto.dart'; part 'model/asset_order.dart'; +part 'model/asset_reject_reason.dart'; part 'model/asset_response_dto.dart'; part 'model/asset_stack_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; +part 'model/asset_upload_action.dart'; part 'model/asset_visibility.dart'; part 'model/audio_codec.dart'; part 'model/auth_status_response_dto.dart'; @@ -142,8 +142,6 @@ part 'model/cq_mode.dart'; part 'model/cast_response.dart'; part 'model/cast_update.dart'; part 'model/change_password_dto.dart'; -part 'model/check_existing_assets_dto.dart'; -part 'model/check_existing_assets_response_dto.dart'; part 'model/colorspace.dart'; part 'model/contributor_count_response_dto.dart'; part 'model/create_album_dto.dart'; @@ -178,7 +176,6 @@ part 'model/job_settings_dto.dart'; part 'model/library_response_dto.dart'; part 'model/library_stats_response_dto.dart'; part 'model/license_key_dto.dart'; -part 'model/license_response_dto.dart'; part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; @@ -242,6 +239,10 @@ part 'model/places_response_dto.dart'; part 'model/plugin_action_response_dto.dart'; part 'model/plugin_context_type.dart'; part 'model/plugin_filter_response_dto.dart'; +part 'model/plugin_json_schema.dart'; +part 'model/plugin_json_schema_property.dart'; +part 'model/plugin_json_schema_property_additional_properties.dart'; +part 'model/plugin_json_schema_type.dart'; part 'model/plugin_response_dto.dart'; part 'model/plugin_trigger_response_dto.dart'; part 'model/plugin_trigger_type.dart'; @@ -283,7 +284,6 @@ part 'model/server_media_types_response_dto.dart'; part 'model/server_ping_response.dart'; part 'model/server_stats_response_dto.dart'; part 'model/server_storage_response_dto.dart'; -part 'model/server_theme_dto.dart'; part 'model/server_version_history_response_dto.dart'; part 'model/server_version_response_dto.dart'; part 'model/session_create_dto.dart'; diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index 697598ac97..e0a393948c 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -136,10 +136,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID @@ -195,10 +193,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e2db95b9e0..d08d1cba9d 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -27,11 +27,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, key: key, slug: slug, ); + Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto,) async { + final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -104,11 +89,7 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { + Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/albums/assets'; @@ -119,13 +100,6 @@ class AlbumsApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -147,12 +121,8 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto, { String? key, String? slug, }) async { - final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto, key: key, slug: slug, ); + Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto,) async { + final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -345,10 +315,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { + Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -366,9 +333,6 @@ class AlbumsApi { if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (withoutAssets != null) { - queryParams.addAll(_queryParams('', 'withoutAssets', withoutAssets)); - } const contentTypes = []; @@ -395,11 +359,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { - final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, ); + Future getAlbumInfo(String id, { String? key, String? slug, }) async { + final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -413,6 +374,81 @@ class AlbumsApi { return null; } + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/albums/{id}/map-markers' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve album map markers + /// + /// Retrieve map marker information for a specific album by its ID. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future?> getAlbumMapMarkers(String id, { String? key, String? slug, }) async { + final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Retrieve album statistics /// /// Returns statistics about the albums available to the authenticated user. diff --git a/mobile/openapi/lib/api/api_keys_api.dart b/mobile/openapi/lib/api/api_keys_api.dart index 0bd26575c6..3ca85265c4 100644 --- a/mobile/openapi/lib/api/api_keys_api.dart +++ b/mobile/openapi/lib/api/api_keys_api.dart @@ -24,13 +24,13 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKeyWithHttpInfo(APIKeyCreateDto aPIKeyCreateDto,) async { + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; // ignore: prefer_final_locals - Object? postBody = aPIKeyCreateDto; + Object? postBody = apiKeyCreateDto; final queryParams = []; final headerParams = {}; @@ -56,9 +56,9 @@ class APIKeysApi { /// /// Parameters: /// - /// * [APIKeyCreateDto] aPIKeyCreateDto (required): - Future createApiKey(APIKeyCreateDto aPIKeyCreateDto,) async { - final response = await createApiKeyWithHttpInfo(aPIKeyCreateDto,); + /// * [ApiKeyCreateDto] apiKeyCreateDto (required): + Future createApiKey(ApiKeyCreateDto apiKeyCreateDto,) async { + final response = await createApiKeyWithHttpInfo(apiKeyCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -66,7 +66,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyCreateResponseDto',) as APIKeyCreateResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyCreateResponseDto',) as ApiKeyCreateResponseDto; } return null; @@ -163,7 +163,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKey(String id,) async { + Future getApiKey(String id,) async { final response = await getApiKeyWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -172,7 +172,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -211,7 +211,7 @@ class APIKeysApi { /// List all API keys /// /// Retrieve all API keys of the current user. - Future?> getApiKeys() async { + Future?> getApiKeys() async { final response = await getApiKeysWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -221,8 +221,8 @@ class APIKeysApi { // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } @@ -262,7 +262,7 @@ class APIKeysApi { /// Retrieve the current API key /// /// Retrieve the API key that is used to access this endpoint. - Future getMyApiKey() async { + Future getMyApiKey() async { final response = await getMyApiKeyWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -271,7 +271,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; @@ -287,14 +287,14 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKeyWithHttpInfo(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = aPIKeyUpdateDto; + Object? postBody = apiKeyUpdateDto; final queryParams = []; final headerParams = {}; @@ -322,9 +322,9 @@ class APIKeysApi { /// /// * [String] id (required): /// - /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required): - Future updateApiKey(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async { - final response = await updateApiKeyWithHttpInfo(id, aPIKeyUpdateDto,); + /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): + Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { + final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -332,7 +332,7 @@ class APIKeysApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'APIKeyResponseDto',) as APIKeyResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto; } return null; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index a026b99028..5046376168 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -72,62 +72,6 @@ class AssetsApi { return null; } - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/exist'; - - // ignore: prefer_final_locals - Object? postBody = checkExistingAssetsDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Check existing assets - /// - /// Checks if multiple assets exist on the server and returns all existing - used by background backup - /// - /// Parameters: - /// - /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): - Future checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async { - final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto; - - } - return null; - } - /// Copy asset /// /// Copy asset information like albums, tags, etc. from one asset to another. @@ -472,68 +416,6 @@ class AssetsApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve edits for an existing asset /// /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. @@ -864,7 +746,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/statistics'; @@ -913,7 +794,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -929,71 +809,6 @@ class AssetsApi { return null; } - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future getRandomWithHttpInfo({ num? count, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/random'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (count != null) { - queryParams.addAll(_queryParams('', 'count', count)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future?> getRandom({ num? count, }) async { - final response = await getRandomWithHttpInfo( count: count, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Play asset video /// /// Streams the video file for the specified asset. This endpoint also supports byte range requests. @@ -1115,154 +930,6 @@ class AssetsApi { } } - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/{id}/original' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - - const contentTypes = ['multipart/form-data']; - - bool hasFields = false; - final mp = MultipartRequest('PUT', Uri.parse(apiPath)); - if (assetData != null) { - hasFields = true; - mp.fields[r'assetData'] = assetData.field; - mp.files.add(assetData); - } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } - if (duration != null) { - hasFields = true; - mp.fields[r'duration'] = parameterToString(duration); - } - if (fileCreatedAt != null) { - hasFields = true; - mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt); - } - if (fileModifiedAt != null) { - hasFields = true; - mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); - } - if (filename != null) { - hasFields = true; - mp.fields[r'filename'] = parameterToString(filename); - } - if (hasFields) { - postBody = mp; - } - - return apiClient.invokeAPI( - apiPath, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto; - - } - return null; - } - /// Run an asset job /// /// Run a specific job on a set of assets. @@ -1554,12 +1221,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1592,8 +1253,7 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility - Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1624,14 +1284,6 @@ class AssetsApi { mp.fields[r'assetData'] = assetData.field; mp.files.add(assetData); } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } if (duration != null) { hasFields = true; mp.fields[r'duration'] = parameterToString(duration); @@ -1693,12 +1345,6 @@ class AssetsApi { /// * [MultipartFile] assetData (required): /// Asset file data /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// /// * [DateTime] fileCreatedAt (required): /// File creation date /// @@ -1731,9 +1377,8 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility - Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); + Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1763,7 +1408,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { @@ -1819,7 +1463,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart index fbd485f86f..768185db1e 100644 --- a/mobile/openapi/lib/api/database_backups_admin_api.dart +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -218,6 +218,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/upload'; @@ -260,6 +261,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackup({ MultipartFile? file, }) async { final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 33bcaf062c..a437cd5837 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -73,183 +73,6 @@ class DeprecatedApi { return null; } - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/device/{deviceId}' - .replaceAll('{deviceId}', deviceId); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Retrieve assets by device ID - /// - /// Get all asset of a device that are in the database, ID only. - /// - /// Parameters: - /// - /// * [String] deviceId (required): - /// Device ID - Future?> getAllUserAssetsByDeviceId(String deviceId,) async { - final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. @@ -298,219 +121,6 @@ class DeprecatedApi { return null; } - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future getRandomWithHttpInfo({ num? count, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/random'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (count != null) { - queryParams.addAll(_queryParams('', 'count', count)); - } - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get random assets - /// - /// Retrieve a specified number of random assets for the authenticated user. - /// - /// Parameters: - /// - /// * [num] count: - /// Number of random assets to return - Future?> getRandom({ num? count, }) async { - final response = await getRandomWithHttpInfo( count: count, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/{id}/original' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - - const contentTypes = ['multipart/form-data']; - - bool hasFields = false; - final mp = MultipartRequest('PUT', Uri.parse(apiPath)); - if (assetData != null) { - hasFields = true; - mp.fields[r'assetData'] = assetData.field; - mp.files.add(assetData); - } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } - if (duration != null) { - hasFields = true; - mp.fields[r'duration'] = parameterToString(duration); - } - if (fileCreatedAt != null) { - hasFields = true; - mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt); - } - if (fileModifiedAt != null) { - hasFields = true; - mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); - } - if (filename != null) { - hasFields = true; - mp.fields[r'filename'] = parameterToString(filename); - } - if (hasFields) { - postBody = mp; - } - - return apiClient.invokeAPI( - apiPath, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto; - - } - return null; - } - /// Run jobs /// /// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets. @@ -520,7 +130,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -556,7 +165,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 41517f8144..9dda59a883 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -121,7 +121,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -157,7 +156,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 913205428e..0cd96ac442 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -260,13 +260,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -327,13 +325,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -431,13 +427,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -498,13 +492,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index d4e2b1d80f..ab0be3e8f3 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -182,10 +182,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status @@ -237,10 +235,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index 3b15b90909..7d18f6d867 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -138,7 +138,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future getPartnersWithHttpInfo(PartnerDirection direction,) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -173,7 +172,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future?> getPartners(PartnerDirection direction,) async { final response = await getPartnersWithHttpInfo(direction,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart index ecb556e434..1312cb5952 100644 --- a/mobile/openapi/lib/api/queues_api.dart +++ b/mobile/openapi/lib/api/queues_api.dart @@ -25,7 +25,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -61,7 +60,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -80,7 +78,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueueWithHttpInfo(QueueName name,) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' @@ -114,7 +111,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueue(QueueName name,) async { final response = await getQueueWithHttpInfo(name,); if (response.statusCode >= HttpStatus.badRequest) { @@ -139,7 +135,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -180,7 +175,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -262,7 +256,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { @@ -298,7 +291,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 085958de66..730627d4a1 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -127,7 +127,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -198,7 +197,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -370,9 +368,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -434,7 +429,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -443,14 +437,13 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets /// /// * [bool] withExif: /// Include EXIF data in response - Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/large-assets'; @@ -476,9 +469,6 @@ class SearchApi { if (createdBefore != null) { queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); } - if (deviceId != null) { - queryParams.addAll(_queryParams('', 'deviceId', deviceId)); - } if (isEncoded != null) { queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); } @@ -593,9 +583,6 @@ class SearchApi { /// * [DateTime] createdBefore: /// Filter by creation date (before) /// - /// * [String] deviceId: - /// Device ID to filter by - /// /// * [bool] isEncoded: /// Filter by encoded status /// @@ -657,7 +644,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -666,15 +652,14 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets /// /// * [bool] withExif: /// Include EXIF data in response - Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { - final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); + Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index f5b70a9ea4..dd38ade167 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -281,7 +281,7 @@ class ServerApi { /// Get product key /// /// Retrieve information about whether the server currently has a product key registered. - Future getServerLicense() async { + Future getServerLicense() async { final response = await getServerLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -290,7 +290,7 @@ class ServerApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -488,54 +488,6 @@ class ServerApi { return null; } - /// Get theme - /// - /// Retrieve the custom CSS, if existent. - /// - /// Note: This method returns the HTTP [Response]. - Future getThemeWithHttpInfo() async { - // ignore: prefer_const_declarations - final apiPath = r'/server/theme'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get theme - /// - /// Retrieve the custom CSS, if existent. - Future getTheme() async { - final response = await getThemeWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerThemeDto',) as ServerThemeDto; - - } - return null; - } - /// Get version check status /// /// Retrieve information about the last time the version check ran. @@ -724,7 +676,7 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { + Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setServerLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -733,7 +685,7 @@ class ServerApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 084662ace8..4750442287 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -27,11 +27,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { + Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -43,13 +39,6 @@ class SharedLinksApi { final headerParams = {}; final formParams = {}; - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - const contentTypes = ['application/json']; @@ -73,12 +62,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - /// - /// * [String] key: - /// - /// * [String] slug: - Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { - final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, key: key, slug: slug, ); + Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { + final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -235,14 +220,8 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async { + Future getMySharedLinkWithHttpInfo({ String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/me'; @@ -256,15 +235,9 @@ class SharedLinksApi { if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } - if (password != null) { - queryParams.addAll(_queryParams('', 'password', password)); - } if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (token != null) { - queryParams.addAll(_queryParams('', 'token', token)); - } const contentTypes = []; @@ -288,15 +261,9 @@ class SharedLinksApi { /// /// * [String] key: /// - /// * [String] password: - /// Link password - /// /// * [String] slug: - /// - /// * [String] token: - /// Access token - Future getMySharedLink({ String? key, String? password, String? slug, String? token, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, ); + Future getMySharedLink({ String? key, String? slug, }) async { + final response = await getMySharedLinkWithHttpInfo( key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/sync_api.dart b/mobile/openapi/lib/api/sync_api.dart index 6194fd0f89..e7bc822ace 100644 --- a/mobile/openapi/lib/api/sync_api.dart +++ b/mobile/openapi/lib/api/sync_api.dart @@ -64,121 +64,6 @@ class SyncApi { } } - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/delta-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetDeltaSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get delta sync for user - /// - /// Retrieve changed assets since the last sync for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required): - Future getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async { - final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto; - - } - return null; - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/sync/full-sync'; - - // ignore: prefer_final_locals - Object? postBody = assetFullSyncDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Get full sync for user - /// - /// Retrieve all assets for a full synchronization for the authenticated user. - /// - /// Parameters: - /// - /// * [AssetFullSyncDto] assetFullSyncDto (required): - Future?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async { - final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Retrieve acknowledgements /// /// Retrieve the synchronization acknowledgments for the current session. diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index ccb5930e3c..282117e8ee 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -25,7 +25,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album @@ -148,7 +148,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index 59a4b60096..5e165ffd5d 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -324,7 +324,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/statistics' @@ -376,7 +375,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index 7ccae02c76..401cf4e94b 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -447,7 +447,7 @@ class UsersApi { /// Retrieve user product key /// /// Retrieve information about whether the current user has a registered product key. - Future getUserLicense() async { + Future getUserLicense() async { final response = await getUserLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -456,7 +456,7 @@ class UsersApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -602,7 +602,7 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { + Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setUserLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -611,7 +611,7 @@ class UsersApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -731,7 +731,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Note: This method returns the HTTP [Response]. /// @@ -765,7 +765,7 @@ class UsersApi { /// Update current user /// - /// Update the current user making teh API request. + /// Update the current user making the API request. /// /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 48e5f5874b..b8799a7be5 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -182,14 +182,6 @@ class ApiClient { return valueString == 'true' || valueString == '1'; case 'DateTime': return value is DateTime ? value : DateTime.tryParse(value); - case 'APIKeyCreateDto': - return APIKeyCreateDto.fromJson(value); - case 'APIKeyCreateResponseDto': - return APIKeyCreateResponseDto.fromJson(value); - case 'APIKeyResponseDto': - return APIKeyResponseDto.fromJson(value); - case 'APIKeyUpdateDto': - return APIKeyUpdateDto.fromJson(value); case 'ActivityCreateDto': return ActivityCreateDto.fromJson(value); case 'ActivityResponseDto': @@ -220,6 +212,14 @@ class ApiClient { return AlbumsResponse.fromJson(value); case 'AlbumsUpdate': return AlbumsUpdate.fromJson(value); + case 'ApiKeyCreateDto': + return ApiKeyCreateDto.fromJson(value); + case 'ApiKeyCreateResponseDto': + return ApiKeyCreateResponseDto.fromJson(value); + case 'ApiKeyResponseDto': + return ApiKeyResponseDto.fromJson(value); + case 'ApiKeyUpdateDto': + return ApiKeyUpdateDto.fromJson(value); case 'AssetBulkDeleteDto': return AssetBulkDeleteDto.fromJson(value); case 'AssetBulkUpdateDto': @@ -234,10 +234,6 @@ class ApiClient { return AssetBulkUploadCheckResult.fromJson(value); case 'AssetCopyDto': return AssetCopyDto.fromJson(value); - case 'AssetDeltaSyncDto': - return AssetDeltaSyncDto.fromJson(value); - case 'AssetDeltaSyncResponseDto': - return AssetDeltaSyncResponseDto.fromJson(value); case 'AssetEditAction': return AssetEditActionTypeTransformer().decode(value); case 'AssetEditActionItemDto': @@ -262,8 +258,8 @@ class ApiClient { return AssetFaceUpdateItem.fromJson(value); case 'AssetFaceWithoutPersonResponseDto': return AssetFaceWithoutPersonResponseDto.fromJson(value); - case 'AssetFullSyncDto': - return AssetFullSyncDto.fromJson(value); + case 'AssetIdErrorReason': + return AssetIdErrorReasonTypeTransformer().decode(value); case 'AssetIdsDto': return AssetIdsDto.fromJson(value); case 'AssetIdsResponseDto': @@ -298,6 +294,8 @@ class ApiClient { return AssetOcrResponseDto.fromJson(value); case 'AssetOrder': return AssetOrderTypeTransformer().decode(value); + case 'AssetRejectReason': + return AssetRejectReasonTypeTransformer().decode(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); case 'AssetStackResponseDto': @@ -306,6 +304,8 @@ class ApiClient { return AssetStatsResponseDto.fromJson(value); case 'AssetTypeEnum': return AssetTypeEnumTypeTransformer().decode(value); + case 'AssetUploadAction': + return AssetUploadActionTypeTransformer().decode(value); case 'AssetVisibility': return AssetVisibilityTypeTransformer().decode(value); case 'AudioCodec': @@ -330,10 +330,6 @@ class ApiClient { return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); - case 'CheckExistingAssetsDto': - return CheckExistingAssetsDto.fromJson(value); - case 'CheckExistingAssetsResponseDto': - return CheckExistingAssetsResponseDto.fromJson(value); case 'Colorspace': return ColorspaceTypeTransformer().decode(value); case 'ContributorCountResponseDto': @@ -402,8 +398,6 @@ class ApiClient { return LibraryStatsResponseDto.fromJson(value); case 'LicenseKeyDto': return LicenseKeyDto.fromJson(value); - case 'LicenseResponseDto': - return LicenseResponseDto.fromJson(value); case 'LogLevel': return LogLevelTypeTransformer().decode(value); case 'LoginCredentialDto': @@ -530,6 +524,14 @@ class ApiClient { return PluginContextTypeTypeTransformer().decode(value); case 'PluginFilterResponseDto': return PluginFilterResponseDto.fromJson(value); + case 'PluginJsonSchema': + return PluginJsonSchema.fromJson(value); + case 'PluginJsonSchemaProperty': + return PluginJsonSchemaProperty.fromJson(value); + case 'PluginJsonSchemaPropertyAdditionalProperties': + return PluginJsonSchemaPropertyAdditionalProperties.fromJson(value); + case 'PluginJsonSchemaType': + return PluginJsonSchemaTypeTypeTransformer().decode(value); case 'PluginResponseDto': return PluginResponseDto.fromJson(value); case 'PluginTriggerResponseDto': @@ -612,8 +614,6 @@ class ApiClient { return ServerStatsResponseDto.fromJson(value); case 'ServerStorageResponseDto': return ServerStorageResponseDto.fromJson(value); - case 'ServerThemeDto': - return ServerThemeDto.fromJson(value); case 'ServerVersionHistoryResponseDto': return ServerVersionHistoryResponseDto.fromJson(value); case 'ServerVersionResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 830325a5b6..3b36b23d6c 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -61,6 +61,9 @@ String parameterToString(dynamic value) { if (value is AssetEditAction) { return AssetEditActionTypeTransformer().encode(value).toString(); } + if (value is AssetIdErrorReason) { + return AssetIdErrorReasonTypeTransformer().encode(value).toString(); + } if (value is AssetJobName) { return AssetJobNameTypeTransformer().encode(value).toString(); } @@ -73,9 +76,15 @@ String parameterToString(dynamic value) { if (value is AssetOrder) { return AssetOrderTypeTransformer().encode(value).toString(); } + if (value is AssetRejectReason) { + return AssetRejectReasonTypeTransformer().encode(value).toString(); + } if (value is AssetTypeEnum) { return AssetTypeEnumTypeTransformer().encode(value).toString(); } + if (value is AssetUploadAction) { + return AssetUploadActionTypeTransformer().encode(value).toString(); + } if (value is AssetVisibility) { return AssetVisibilityTypeTransformer().encode(value).toString(); } @@ -133,6 +142,9 @@ String parameterToString(dynamic value) { if (value is PluginContextType) { return PluginContextTypeTypeTransformer().encode(value).toString(); } + if (value is PluginJsonSchemaType) { + return PluginJsonSchemaTypeTypeTransformer().encode(value).toString(); + } if (value is PluginTriggerType) { return PluginTriggerTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index fb4b6d084e..bc220e64ce 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -40,7 +40,6 @@ class ActivityCreateDto { /// String? comment; - /// Activity type (like or comment) ReactionType type; @override diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index dadb45d8ac..1b0e279ab7 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -33,7 +33,6 @@ class ActivityResponseDto { /// Activity ID String id; - /// Activity type ReactionType type; UserResponseDto user; @@ -72,7 +71,9 @@ class ActivityResponseDto { } else { // json[r'comment'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'type'] = this.type; json[r'user'] = this.user; @@ -90,7 +91,7 @@ class ActivityResponseDto { return ActivityResponseDto( assetId: mapValueOfType(json, r'assetId'), comment: mapValueOfType(json, r'comment'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, type: ReactionType.fromJson(json[r'type'])!, user: UserResponseDto.fromJson(json[r'user'])!, diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 15ad2a170e..d9ac019ee2 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -18,9 +18,15 @@ class ActivityStatisticsResponseDto { }); /// Number of comments + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int comments; /// Number of likes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int likes; @override diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e686fbdc..348e25ddaf 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -17,7 +17,6 @@ class AlbumResponseDto { required this.albumThumbnailAssetId, this.albumUsers = const [], required this.assetCount, - this.assets = const [], this.contributorCounts = const [], required this.createdAt, required this.description, @@ -43,10 +42,11 @@ class AlbumResponseDto { List albumUsers; /// Number of assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; - List assets; - List contributorCounts; /// Creation date @@ -82,7 +82,6 @@ class AlbumResponseDto { /// DateTime? lastModifiedAssetTimestamp; - /// Asset sort order /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -117,7 +116,6 @@ class AlbumResponseDto { other.albumThumbnailAssetId == albumThumbnailAssetId && _deepEquality.equals(other.albumUsers, albumUsers) && other.assetCount == assetCount && - _deepEquality.equals(other.assets, assets) && _deepEquality.equals(other.contributorCounts, contributorCounts) && other.createdAt == createdAt && other.description == description && @@ -140,7 +138,6 @@ class AlbumResponseDto { (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (albumUsers.hashCode) + (assetCount.hashCode) + - (assets.hashCode) + (contributorCounts.hashCode) + (createdAt.hashCode) + (description.hashCode) + @@ -157,7 +154,7 @@ class AlbumResponseDto { (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; + String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -169,7 +166,6 @@ class AlbumResponseDto { } json[r'albumUsers'] = this.albumUsers; json[r'assetCount'] = this.assetCount; - json[r'assets'] = this.assets; json[r'contributorCounts'] = this.contributorCounts; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; @@ -216,7 +212,6 @@ class AlbumResponseDto { albumThumbnailAssetId: mapValueOfType(json, r'albumThumbnailAssetId'), albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), assetCount: mapValueOfType(json, r'assetCount')!, - assets: AssetResponseDto.listFromJson(json[r'assets']), contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), createdAt: mapDateTime(json, r'createdAt', r'')!, description: mapValueOfType(json, r'description')!, @@ -282,7 +277,6 @@ class AlbumResponseDto { 'albumThumbnailAssetId', 'albumUsers', 'assetCount', - 'assets', 'createdAt', 'description', 'hasSharedLink', diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart index 127334e687..0f440d572d 100644 --- a/mobile/openapi/lib/model/album_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -19,12 +19,21 @@ class AlbumStatisticsResponseDto { }); /// Number of non-shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int notShared; /// Number of owned albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int owned; /// Number of shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int shared; @override diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index c448a0b4b7..ee457905bd 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -13,12 +13,17 @@ part of openapi.api; class AlbumUserAddDto { /// Returns a new [AlbumUserAddDto] instance. AlbumUserAddDto({ - this.role = AlbumUserRole.editor, + this.role, required this.userId, }); - /// Album user role - AlbumUserRole role; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AlbumUserRole? role; /// User ID String userId; @@ -31,7 +36,7 @@ class AlbumUserAddDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (role.hashCode) + + (role == null ? 0 : role!.hashCode) + (userId.hashCode); @override @@ -39,7 +44,11 @@ class AlbumUserAddDto { Map toJson() { final json = {}; + if (this.role != null) { json[r'role'] = this.role; + } else { + // json[r'role'] = null; + } json[r'userId'] = this.userId; return json; } @@ -53,7 +62,7 @@ class AlbumUserAddDto { final json = value.cast(); return AlbumUserAddDto( - role: AlbumUserRole.fromJson(json[r'role']) ?? AlbumUserRole.editor, + role: AlbumUserRole.fromJson(json[r'role']), userId: mapValueOfType(json, r'userId')!, ); } diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart index 8006748341..26aa35ae78 100644 --- a/mobile/openapi/lib/model/album_user_create_dto.dart +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -17,7 +17,6 @@ class AlbumUserCreateDto { required this.userId, }); - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 8d0c01cfb8..bbae03fba7 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -17,7 +17,6 @@ class AlbumUserResponseDto { required this.user, }); - /// Album user role AlbumUserRole role; UserResponseDto user; diff --git a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart index 743a9f0645..99e679222e 100644 --- a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart +++ b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart @@ -17,7 +17,6 @@ class AlbumsAddAssetsResponseDto { required this.success, }); - /// Error reason /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/albums_response.dart b/mobile/openapi/lib/model/albums_response.dart index 520ee171c1..def205de90 100644 --- a/mobile/openapi/lib/model/albums_response.dart +++ b/mobile/openapi/lib/model/albums_response.dart @@ -13,10 +13,9 @@ part of openapi.api; class AlbumsResponse { /// Returns a new [AlbumsResponse] instance. AlbumsResponse({ - this.defaultAssetOrder = AssetOrder.desc, + required this.defaultAssetOrder, }); - /// Default asset order for albums AssetOrder defaultAssetOrder; @override diff --git a/mobile/openapi/lib/model/albums_update.dart b/mobile/openapi/lib/model/albums_update.dart index 107c65dd1e..d61b5c1398 100644 --- a/mobile/openapi/lib/model/albums_update.dart +++ b/mobile/openapi/lib/model/albums_update.dart @@ -16,7 +16,6 @@ class AlbumsUpdate { this.defaultAssetOrder, }); - /// Default asset order for albums /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index e64b127820..6d3ffc1eb1 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyCreateDto { - /// Returns a new [APIKeyCreateDto] instance. - APIKeyCreateDto({ +class ApiKeyCreateDto { + /// Returns a new [ApiKeyCreateDto] instance. + ApiKeyCreateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyCreateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyCreateDto { (permissions.hashCode); @override - String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyCreateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyCreateDto { return json; } - /// Returns a new [APIKeyCreateDto] instance and imports its values from + /// Returns a new [ApiKeyCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateDto"); + static ApiKeyCreateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateDto( + return ApiKeyCreateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyCreateDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyCreateDto.fromJson(row); + final value = ApiKeyCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyCreateDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyCreateDto.fromJson(entry.value); + final value = ApiKeyCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyCreateDto { return map; } - // maps a json object with a list of APIKeyCreateDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyCreateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart index 7540c4bb26..77b19ebfd2 100644 --- a/mobile/openapi/lib/model/api_key_create_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -10,20 +10,20 @@ part of openapi.api; -class APIKeyCreateResponseDto { - /// Returns a new [APIKeyCreateResponseDto] instance. - APIKeyCreateResponseDto({ +class ApiKeyCreateResponseDto { + /// Returns a new [ApiKeyCreateResponseDto] instance. + ApiKeyCreateResponseDto({ required this.apiKey, required this.secret, }); - APIKeyResponseDto apiKey; + ApiKeyResponseDto apiKey; /// API key secret (only shown once) String secret; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyCreateResponseDto && other.apiKey == apiKey && other.secret == secret; @@ -34,7 +34,7 @@ class APIKeyCreateResponseDto { (secret.hashCode); @override - String toString() => 'APIKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; + String toString() => 'ApiKeyCreateResponseDto[apiKey=$apiKey, secret=$secret]'; Map toJson() { final json = {}; @@ -43,27 +43,27 @@ class APIKeyCreateResponseDto { return json; } - /// Returns a new [APIKeyCreateResponseDto] instance and imports its values from + /// Returns a new [ApiKeyCreateResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyCreateResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyCreateResponseDto"); + static ApiKeyCreateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyCreateResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyCreateResponseDto( - apiKey: APIKeyResponseDto.fromJson(json[r'apiKey'])!, + return ApiKeyCreateResponseDto( + apiKey: ApiKeyResponseDto.fromJson(json[r'apiKey'])!, secret: mapValueOfType(json, r'secret')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyCreateResponseDto.fromJson(row); + final value = ApiKeyCreateResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +72,12 @@ class APIKeyCreateResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyCreateResponseDto.fromJson(entry.value); + final value = ApiKeyCreateResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +86,14 @@ class APIKeyCreateResponseDto { return map; } - // maps a json object with a list of APIKeyCreateResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyCreateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyCreateResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index 32ba543342..79099188a3 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyResponseDto { - /// Returns a new [APIKeyResponseDto] instance. - APIKeyResponseDto({ +class ApiKeyResponseDto { + /// Returns a new [ApiKeyResponseDto] instance. + ApiKeyResponseDto({ required this.createdAt, required this.id, required this.name, @@ -36,7 +36,7 @@ class APIKeyResponseDto { DateTime updatedAt; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyResponseDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyResponseDto && other.createdAt == createdAt && other.id == id && other.name == name && @@ -53,42 +53,46 @@ class APIKeyResponseDto { (updatedAt.hashCode); @override - String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; + String toString() => 'ApiKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; json[r'permissions'] = this.permissions; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } - /// Returns a new [APIKeyResponseDto] instance and imports its values from + /// Returns a new [ApiKeyResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyResponseDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyResponseDto"); + static ApiKeyResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyResponseDto"); if (value is Map) { final json = value.cast(); - return APIKeyResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + return ApiKeyResponseDto( + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, permissions: Permission.listFromJson(json[r'permissions']), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyResponseDto.fromJson(row); + final value = ApiKeyResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -97,12 +101,12 @@ class APIKeyResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyResponseDto.fromJson(entry.value); + final value = ApiKeyResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -111,14 +115,14 @@ class APIKeyResponseDto { return map; } - // maps a json object with a list of APIKeyResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index ba107bcda2..c8df4be654 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class APIKeyUpdateDto { - /// Returns a new [APIKeyUpdateDto] instance. - APIKeyUpdateDto({ +class ApiKeyUpdateDto { + /// Returns a new [ApiKeyUpdateDto] instance. + ApiKeyUpdateDto({ this.name, this.permissions = const [], }); @@ -30,7 +30,7 @@ class APIKeyUpdateDto { List permissions; @override - bool operator ==(Object other) => identical(this, other) || other is APIKeyUpdateDto && + bool operator ==(Object other) => identical(this, other) || other is ApiKeyUpdateDto && other.name == name && _deepEquality.equals(other.permissions, permissions); @@ -41,7 +41,7 @@ class APIKeyUpdateDto { (permissions.hashCode); @override - String toString() => 'APIKeyUpdateDto[name=$name, permissions=$permissions]'; + String toString() => 'ApiKeyUpdateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -54,15 +54,15 @@ class APIKeyUpdateDto { return json; } - /// Returns a new [APIKeyUpdateDto] instance and imports its values from + /// Returns a new [ApiKeyUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static APIKeyUpdateDto? fromJson(dynamic value) { - upgradeDto(value, "APIKeyUpdateDto"); + static ApiKeyUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "ApiKeyUpdateDto"); if (value is Map) { final json = value.cast(); - return APIKeyUpdateDto( + return ApiKeyUpdateDto( name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); @@ -70,11 +70,11 @@ class APIKeyUpdateDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = APIKeyUpdateDto.fromJson(row); + final value = ApiKeyUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -83,12 +83,12 @@ class APIKeyUpdateDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = APIKeyUpdateDto.fromJson(entry.value); + final value = ApiKeyUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -97,14 +97,14 @@ class APIKeyUpdateDto { return map; } - // maps a json object with a list of APIKeyUpdateDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of ApiKeyUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = APIKeyUpdateDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = ApiKeyUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 99bac7abfa..f97300b19f 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -70,6 +70,9 @@ class AssetBulkUpdateDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -79,6 +82,9 @@ class AssetBulkUpdateDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -90,7 +96,7 @@ class AssetBulkUpdateDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; /// Time zone (IANA timezone) /// @@ -101,7 +107,6 @@ class AssetBulkUpdateDto { /// String? timeZone; - /// Asset visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -217,9 +222,7 @@ class AssetBulkUpdateDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), timeZone: mapValueOfType(json, r'timeZone'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index b56370f689..bf3ee8e244 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -20,8 +20,7 @@ class AssetBulkUploadCheckResult { this.reason, }); - /// Upload action - AssetBulkUploadCheckResultActionEnum action; + AssetUploadAction action; /// Existing asset ID if duplicate /// @@ -44,8 +43,13 @@ class AssetBulkUploadCheckResult { /// bool? isTrashed; - /// Rejection reason if rejected - AssetBulkUploadCheckResultReasonEnum? reason; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetRejectReason? reason; @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResult && @@ -98,11 +102,11 @@ class AssetBulkUploadCheckResult { final json = value.cast(); return AssetBulkUploadCheckResult( - action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!, + action: AssetUploadAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId'), id: mapValueOfType(json, r'id')!, isTrashed: mapValueOfType(json, r'isTrashed'), - reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']), + reason: AssetRejectReason.fromJson(json[r'reason']), ); } return null; @@ -155,151 +159,3 @@ class AssetBulkUploadCheckResult { }; } -/// Upload action -class AssetBulkUploadCheckResultActionEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultActionEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const accept = AssetBulkUploadCheckResultActionEnum._(r'accept'); - static const reject = AssetBulkUploadCheckResultActionEnum._(r'reject'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultActionEnum]. - static const values = [ - accept, - reject, - ]; - - static AssetBulkUploadCheckResultActionEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultActionEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultActionEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultActionEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultActionEnum]. -class AssetBulkUploadCheckResultActionEnumTypeTransformer { - factory AssetBulkUploadCheckResultActionEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultActionEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultActionEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultActionEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'accept': return AssetBulkUploadCheckResultActionEnum.accept; - case r'reject': return AssetBulkUploadCheckResultActionEnum.reject; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultActionEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultActionEnumTypeTransformer? _instance; -} - - -/// Rejection reason if rejected -class AssetBulkUploadCheckResultReasonEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultReasonEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetBulkUploadCheckResultReasonEnum._(r'duplicate'); - static const unsupportedFormat = AssetBulkUploadCheckResultReasonEnum._(r'unsupported-format'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultReasonEnum]. - static const values = [ - duplicate, - unsupportedFormat, - ]; - - static AssetBulkUploadCheckResultReasonEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultReasonEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultReasonEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultReasonEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultReasonEnum]. -class AssetBulkUploadCheckResultReasonEnumTypeTransformer { - factory AssetBulkUploadCheckResultReasonEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultReasonEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultReasonEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultReasonEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetBulkUploadCheckResultReasonEnum.duplicate; - case r'unsupported-format': return AssetBulkUploadCheckResultReasonEnum.unsupportedFormat; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultReasonEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultReasonEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart deleted file mode 100644 index 22c09752d2..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ /dev/null @@ -1,111 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetDeltaSyncDto { - /// Returns a new [AssetDeltaSyncDto] instance. - AssetDeltaSyncDto({ - required this.updatedAfter, - this.userIds = const [], - }); - - /// Sync assets updated after this date - DateTime updatedAfter; - - /// User IDs to sync - List userIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncDto && - other.updatedAfter == updatedAfter && - _deepEquality.equals(other.userIds, userIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (updatedAfter.hashCode) + - (userIds.hashCode); - - @override - String toString() => 'AssetDeltaSyncDto[updatedAfter=$updatedAfter, userIds=$userIds]'; - - Map toJson() { - final json = {}; - json[r'updatedAfter'] = this.updatedAfter.toUtc().toIso8601String(); - json[r'userIds'] = this.userIds; - return json; - } - - /// Returns a new [AssetDeltaSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncDto( - updatedAfter: mapDateTime(json, r'updatedAfter', r'')!, - userIds: json[r'userIds'] is Iterable - ? (json[r'userIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetDeltaSyncDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetDeltaSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetDeltaSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'updatedAfter', - 'userIds', - }; -} - diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart deleted file mode 100644 index 7351840b11..0000000000 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ /dev/null @@ -1,120 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetDeltaSyncResponseDto { - /// Returns a new [AssetDeltaSyncResponseDto] instance. - AssetDeltaSyncResponseDto({ - this.deleted = const [], - required this.needsFullSync, - this.upserted = const [], - }); - - /// Deleted asset IDs - List deleted; - - /// Whether full sync is needed - bool needsFullSync; - - /// Upserted assets - List upserted; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetDeltaSyncResponseDto && - _deepEquality.equals(other.deleted, deleted) && - other.needsFullSync == needsFullSync && - _deepEquality.equals(other.upserted, upserted); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deleted.hashCode) + - (needsFullSync.hashCode) + - (upserted.hashCode); - - @override - String toString() => 'AssetDeltaSyncResponseDto[deleted=$deleted, needsFullSync=$needsFullSync, upserted=$upserted]'; - - Map toJson() { - final json = {}; - json[r'deleted'] = this.deleted; - json[r'needsFullSync'] = this.needsFullSync; - json[r'upserted'] = this.upserted; - return json; - } - - /// Returns a new [AssetDeltaSyncResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetDeltaSyncResponseDto? fromJson(dynamic value) { - upgradeDto(value, "AssetDeltaSyncResponseDto"); - if (value is Map) { - final json = value.cast(); - - return AssetDeltaSyncResponseDto( - deleted: json[r'deleted'] is Iterable - ? (json[r'deleted'] as Iterable).cast().toList(growable: false) - : const [], - needsFullSync: mapValueOfType(json, r'needsFullSync')!, - upserted: AssetResponseDto.listFromJson(json[r'upserted']), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetDeltaSyncResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetDeltaSyncResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetDeltaSyncResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetDeltaSyncResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deleted', - 'needsFullSync', - 'upserted', - }; -} - diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart index 7829de4bd5..1b19612bf3 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart @@ -17,10 +17,9 @@ class AssetEditActionItemDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; - AssetEditActionItemDtoParameters parameters; + Map parameters; @override bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && @@ -53,7 +52,7 @@ class AssetEditActionItemDto { return AssetEditActionItemDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, + parameters: json[r'parameters'], ); } return null; diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart index fc67aa022f..2086f72929 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart @@ -44,7 +44,6 @@ class AssetEditActionItemDtoParameters { /// Rotation angle in degrees num angle; - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart index a23a1ef5f3..3315fe8579 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart @@ -18,9 +18,9 @@ class AssetEditActionItemResponseDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; + /// Asset edit ID String id; AssetEditActionItemDtoParameters parameters; diff --git a/mobile/openapi/lib/model/asset_face_create_dto.dart b/mobile/openapi/lib/model/asset_face_create_dto.dart index 3ecc20c699..29c28175cd 100644 --- a/mobile/openapi/lib/model/asset_face_create_dto.dart +++ b/mobile/openapi/lib/model/asset_face_create_dto.dart @@ -27,24 +27,42 @@ class AssetFaceCreateDto { String assetId; /// Face bounding box height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int height; /// Image height in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID String personId; /// Face bounding box width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int width; /// Face bounding box X coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int x; /// Face bounding box Y coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int y; @override diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 61d972a0c4..21b86dfe4e 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -25,30 +25,46 @@ class AssetFaceResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Person associated with face PersonResponseDto? person; - /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index 1ae5cef07e..4a4a2a658e 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -24,27 +24,44 @@ class AssetFaceWithoutPersonResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart deleted file mode 100644 index 3fabb1cac6..0000000000 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ /dev/null @@ -1,147 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetFullSyncDto { - /// Returns a new [AssetFullSyncDto] instance. - AssetFullSyncDto({ - this.lastId, - required this.limit, - required this.updatedUntil, - this.userId, - }); - - /// Last asset ID (pagination) - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? lastId; - - /// Maximum number of assets to return - /// - /// Minimum value: 1 - int limit; - - /// Sync assets updated until this date - DateTime updatedUntil; - - /// Filter by user ID - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? userId; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto && - other.lastId == lastId && - other.limit == limit && - other.updatedUntil == updatedUntil && - other.userId == userId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (lastId == null ? 0 : lastId!.hashCode) + - (limit.hashCode) + - (updatedUntil.hashCode) + - (userId == null ? 0 : userId!.hashCode); - - @override - String toString() => 'AssetFullSyncDto[lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]'; - - Map toJson() { - final json = {}; - if (this.lastId != null) { - json[r'lastId'] = this.lastId; - } else { - // json[r'lastId'] = null; - } - json[r'limit'] = this.limit; - json[r'updatedUntil'] = this.updatedUntil.toUtc().toIso8601String(); - if (this.userId != null) { - json[r'userId'] = this.userId; - } else { - // json[r'userId'] = null; - } - return json; - } - - /// Returns a new [AssetFullSyncDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetFullSyncDto? fromJson(dynamic value) { - upgradeDto(value, "AssetFullSyncDto"); - if (value is Map) { - final json = value.cast(); - - return AssetFullSyncDto( - lastId: mapValueOfType(json, r'lastId'), - limit: mapValueOfType(json, r'limit')!, - updatedUntil: mapDateTime(json, r'updatedUntil', r'')!, - userId: mapValueOfType(json, r'userId'), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetFullSyncDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetFullSyncDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetFullSyncDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetFullSyncDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'limit', - 'updatedUntil', - }; -} - diff --git a/mobile/openapi/lib/model/asset_id_error_reason.dart b/mobile/openapi/lib/model/asset_id_error_reason.dart new file mode 100644 index 0000000000..c51eab1692 --- /dev/null +++ b/mobile/openapi/lib/model/asset_id_error_reason.dart @@ -0,0 +1,88 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Error reason if failed +class AssetIdErrorReason { + /// Instantiate a new enum with the provided [value]. + const AssetIdErrorReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetIdErrorReason._(r'duplicate'); + static const noPermission = AssetIdErrorReason._(r'no_permission'); + static const notFound = AssetIdErrorReason._(r'not_found'); + + /// List of all possible values in this [enum][AssetIdErrorReason]. + static const values = [ + duplicate, + noPermission, + notFound, + ]; + + static AssetIdErrorReason? fromJson(dynamic value) => AssetIdErrorReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetIdErrorReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetIdErrorReason] to String, +/// and [decode] dynamic data back to [AssetIdErrorReason]. +class AssetIdErrorReasonTypeTransformer { + factory AssetIdErrorReasonTypeTransformer() => _instance ??= const AssetIdErrorReasonTypeTransformer._(); + + const AssetIdErrorReasonTypeTransformer._(); + + String encode(AssetIdErrorReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetIdErrorReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetIdErrorReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetIdErrorReason.duplicate; + case r'no_permission': return AssetIdErrorReason.noPermission; + case r'not_found': return AssetIdErrorReason.notFound; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetIdErrorReasonTypeTransformer] instance. + static AssetIdErrorReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart index 9745283021..cafe1b21b9 100644 --- a/mobile/openapi/lib/model/asset_ids_response_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_response_dto.dart @@ -21,8 +21,13 @@ class AssetIdsResponseDto { /// Asset ID String assetId; - /// Error reason if failed - AssetIdsResponseDtoErrorEnum? error; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetIdErrorReason? error; /// Whether operation succeeded bool success; @@ -65,7 +70,7 @@ class AssetIdsResponseDto { return AssetIdsResponseDto( assetId: mapValueOfType(json, r'assetId')!, - error: AssetIdsResponseDtoErrorEnum.fromJson(json[r'error']), + error: AssetIdErrorReason.fromJson(json[r'error']), success: mapValueOfType(json, r'success')!, ); } @@ -119,80 +124,3 @@ class AssetIdsResponseDto { }; } -/// Error reason if failed -class AssetIdsResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const AssetIdsResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetIdsResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = AssetIdsResponseDtoErrorEnum._(r'no_permission'); - static const notFound = AssetIdsResponseDtoErrorEnum._(r'not_found'); - - /// List of all possible values in this [enum][AssetIdsResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - ]; - - static AssetIdsResponseDtoErrorEnum? fromJson(dynamic value) => AssetIdsResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetIdsResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetIdsResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [AssetIdsResponseDtoErrorEnum]. -class AssetIdsResponseDtoErrorEnumTypeTransformer { - factory AssetIdsResponseDtoErrorEnumTypeTransformer() => _instance ??= const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - String encode(AssetIdsResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetIdsResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetIdsResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetIdsResponseDtoErrorEnum.duplicate; - case r'no_permission': return AssetIdsResponseDtoErrorEnum.noPermission; - case r'not_found': return AssetIdsResponseDtoErrorEnum.notFound; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetIdsResponseDtoErrorEnumTypeTransformer] instance. - static AssetIdsResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart index 0aa5544a3a..5085e3820c 100644 --- a/mobile/openapi/lib/model/asset_jobs_dto.dart +++ b/mobile/openapi/lib/model/asset_jobs_dto.dart @@ -20,7 +20,6 @@ class AssetJobsDto { /// Asset IDs List assetIds; - /// Job name AssetJobName name; @override diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart index 905e738b6e..6dc5cd3c92 100644 --- a/mobile/openapi/lib/model/asset_media_response_dto.dart +++ b/mobile/openapi/lib/model/asset_media_response_dto.dart @@ -20,7 +20,6 @@ class AssetMediaResponseDto { /// Asset media ID String id; - /// Upload status AssetMediaStatus status; @override diff --git a/mobile/openapi/lib/model/asset_media_size.dart b/mobile/openapi/lib/model/asset_media_size.dart index 087d19da1f..ed7a72a613 100644 --- a/mobile/openapi/lib/model/asset_media_size.dart +++ b/mobile/openapi/lib/model/asset_media_size.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Asset media size class AssetMediaSize { /// Instantiate a new enum with the provided [value]. const AssetMediaSize._(this.value); diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart index b79a693726..3e16ed8721 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart @@ -16,7 +16,7 @@ class AssetMetadataBulkResponseDto { required this.assetId, required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Asset ID @@ -29,14 +29,14 @@ class AssetMetadataBulkResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkResponseDto && other.assetId == assetId && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -53,7 +53,9 @@ class AssetMetadataBulkResponseDto { final json = {}; json[r'assetId'] = this.assetId; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -69,8 +71,8 @@ class AssetMetadataBulkResponseDto { return AssetMetadataBulkResponseDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart index caaf379b30..e4eab08bf1 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataBulkUpsertItemDto { AssetMetadataBulkUpsertItemDto({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class AssetMetadataBulkUpsertItemDto { String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertItemDto && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class AssetMetadataBulkUpsertItemDto { return AssetMetadataBulkUpsertItemDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_response_dto.dart index 2c3faab178..d3562f5a48 100644 --- a/mobile/openapi/lib/model/asset_metadata_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_response_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataResponseDto { AssetMetadataResponseDto({ required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Metadata key @@ -25,13 +25,13 @@ class AssetMetadataResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataResponseDto && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -46,7 +46,9 @@ class AssetMetadataResponseDto { Map toJson() { final json = {}; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -61,8 +63,8 @@ class AssetMetadataResponseDto { return AssetMetadataResponseDto( key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart index 8a6bcb9b01..70de1941f3 100644 --- a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart @@ -14,19 +14,19 @@ class AssetMetadataUpsertItemDto { /// Returns a new [AssetMetadataUpsertItemDto] instance. AssetMetadataUpsertItemDto({ required this.key, - required this.value, + this.value = const {}, }); /// Metadata key String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertItemDto && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -54,7 +54,7 @@ class AssetMetadataUpsertItemDto { return AssetMetadataUpsertItemDto( key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_reject_reason.dart b/mobile/openapi/lib/model/asset_reject_reason.dart new file mode 100644 index 0000000000..a31e1e6117 --- /dev/null +++ b/mobile/openapi/lib/model/asset_reject_reason.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Rejection reason if rejected +class AssetRejectReason { + /// Instantiate a new enum with the provided [value]. + const AssetRejectReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetRejectReason._(r'duplicate'); + static const unsupportedFormat = AssetRejectReason._(r'unsupported-format'); + + /// List of all possible values in this [enum][AssetRejectReason]. + static const values = [ + duplicate, + unsupportedFormat, + ]; + + static AssetRejectReason? fromJson(dynamic value) => AssetRejectReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetRejectReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetRejectReason] to String, +/// and [decode] dynamic data back to [AssetRejectReason]. +class AssetRejectReasonTypeTransformer { + factory AssetRejectReasonTypeTransformer() => _instance ??= const AssetRejectReasonTypeTransformer._(); + + const AssetRejectReasonTypeTransformer._(); + + String encode(AssetRejectReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetRejectReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetRejectReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetRejectReason.duplicate; + case r'unsupported-format': return AssetRejectReason.unsupportedFormat; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetRejectReasonTypeTransformer] instance. + static AssetRejectReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 078dd0bdaf..324d12fcbf 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -15,8 +15,6 @@ class AssetResponseDto { AssetResponseDto({ required this.checksum, required this.createdAt, - required this.deviceAssetId, - required this.deviceId, this.duplicateId, required this.duration, this.exifInfo, @@ -56,17 +54,11 @@ class AssetResponseDto { /// The UTC timestamp when the asset was originally uploaded to Immich. DateTime createdAt; - /// Device asset ID - String deviceAssetId; - - /// Device ID - String deviceId; - /// Duplicate group ID String? duplicateId; - /// Video duration (for videos) - String duration; + /// Video/gif duration in hh:mm:ss.SSS format (null for static images) + String? duration; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -86,6 +78,8 @@ class AssetResponseDto { bool hasMetadata; /// Asset height + /// + /// Minimum value: 0 num? height; /// Asset ID @@ -159,7 +153,6 @@ class AssetResponseDto { /// Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. String? thumbhash; - /// Asset type AssetTypeEnum type; List unassignedFaces; @@ -167,18 +160,17 @@ class AssetResponseDto { /// The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. DateTime updatedAt; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: 0 num? width; @override bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && other.checksum == checksum && other.createdAt == createdAt && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.duplicateId == duplicateId && other.duration == duration && other.exifInfo == exifInfo && @@ -216,10 +208,8 @@ class AssetResponseDto { // ignore: unnecessary_parenthesis (checksum.hashCode) + (createdAt.hashCode) + - (deviceAssetId.hashCode) + - (deviceId.hashCode) + (duplicateId == null ? 0 : duplicateId!.hashCode) + - (duration.hashCode) + + (duration == null ? 0 : duration!.hashCode) + (exifInfo == null ? 0 : exifInfo!.hashCode) + (fileCreatedAt.hashCode) + (fileModifiedAt.hashCode) + @@ -251,20 +241,22 @@ class AssetResponseDto { (width == null ? 0 : width!.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; + String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; json[r'checksum'] = this.checksum; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - json[r'deviceAssetId'] = this.deviceAssetId; - json[r'deviceId'] = this.deviceId; if (this.duplicateId != null) { json[r'duplicateId'] = this.duplicateId; } else { // json[r'duplicateId'] = null; } + if (this.duration != null) { json[r'duration'] = this.duration; + } else { + // json[r'duration'] = null; + } if (this.exifInfo != null) { json[r'exifInfo'] = this.exifInfo; } else { @@ -348,10 +340,8 @@ class AssetResponseDto { return AssetResponseDto( checksum: mapValueOfType(json, r'checksum')!, createdAt: mapDateTime(json, r'createdAt', r'')!, - deviceAssetId: mapValueOfType(json, r'deviceAssetId')!, - deviceId: mapValueOfType(json, r'deviceId')!, duplicateId: mapValueOfType(json, r'duplicateId'), - duration: mapValueOfType(json, r'duration')!, + duration: mapValueOfType(json, r'duration'), exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, @@ -434,8 +424,6 @@ class AssetResponseDto { static const requiredKeys = { 'checksum', 'createdAt', - 'deviceAssetId', - 'deviceId', 'duration', 'fileCreatedAt', 'fileModifiedAt', diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index 229e7aa710..96fd66a392 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -19,6 +19,9 @@ class AssetStackResponseDto { }); /// Number of assets in stack + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// Stack ID diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart index 201550c87f..df2762a2f3 100644 --- a/mobile/openapi/lib/model/asset_stats_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -19,12 +19,21 @@ class AssetStatsResponseDto { }); /// Number of images + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int images; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/asset_upload_action.dart b/mobile/openapi/lib/model/asset_upload_action.dart new file mode 100644 index 0000000000..b5cdbb0151 --- /dev/null +++ b/mobile/openapi/lib/model/asset_upload_action.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Upload action +class AssetUploadAction { + /// Instantiate a new enum with the provided [value]. + const AssetUploadAction._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const accept = AssetUploadAction._(r'accept'); + static const reject = AssetUploadAction._(r'reject'); + + /// List of all possible values in this [enum][AssetUploadAction]. + static const values = [ + accept, + reject, + ]; + + static AssetUploadAction? fromJson(dynamic value) => AssetUploadActionTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetUploadAction.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetUploadAction] to String, +/// and [decode] dynamic data back to [AssetUploadAction]. +class AssetUploadActionTypeTransformer { + factory AssetUploadActionTypeTransformer() => _instance ??= const AssetUploadActionTypeTransformer._(); + + const AssetUploadActionTypeTransformer._(); + + String encode(AssetUploadAction data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetUploadAction. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetUploadAction? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'accept': return AssetUploadAction.accept; + case r'reject': return AssetUploadAction.reject; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetUploadActionTypeTransformer] instance. + static AssetUploadActionTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart index a817832dab..875eb138a8 100644 --- a/mobile/openapi/lib/model/avatar_update.dart +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -16,7 +16,6 @@ class AvatarUpdate { this.color, }); - /// Avatar color /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index 1fa8536964..bb3f1d8856 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -19,8 +19,13 @@ class BulkIdResponseDto { required this.success, }); - /// Error reason if failed - BulkIdResponseDtoErrorEnum? error; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + BulkIdErrorReason? error; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -80,7 +85,7 @@ class BulkIdResponseDto { final json = value.cast(); return BulkIdResponseDto( - error: BulkIdResponseDtoErrorEnum.fromJson(json[r'error']), + error: BulkIdErrorReason.fromJson(json[r'error']), errorMessage: mapValueOfType(json, r'errorMessage'), id: mapValueOfType(json, r'id')!, success: mapValueOfType(json, r'success')!, @@ -136,86 +141,3 @@ class BulkIdResponseDto { }; } -/// Error reason if failed -class BulkIdResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const BulkIdResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = BulkIdResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = BulkIdResponseDtoErrorEnum._(r'no_permission'); - static const notFound = BulkIdResponseDtoErrorEnum._(r'not_found'); - static const unknown = BulkIdResponseDtoErrorEnum._(r'unknown'); - static const validation = BulkIdResponseDtoErrorEnum._(r'validation'); - - /// List of all possible values in this [enum][BulkIdResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - unknown, - validation, - ]; - - static BulkIdResponseDtoErrorEnum? fromJson(dynamic value) => BulkIdResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = BulkIdResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [BulkIdResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [BulkIdResponseDtoErrorEnum]. -class BulkIdResponseDtoErrorEnumTypeTransformer { - factory BulkIdResponseDtoErrorEnumTypeTransformer() => _instance ??= const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - String encode(BulkIdResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a BulkIdResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - BulkIdResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return BulkIdResponseDtoErrorEnum.duplicate; - case r'no_permission': return BulkIdResponseDtoErrorEnum.noPermission; - case r'not_found': return BulkIdResponseDtoErrorEnum.notFound; - case r'unknown': return BulkIdResponseDtoErrorEnum.unknown; - case r'validation': return BulkIdResponseDtoErrorEnum.validation; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [BulkIdResponseDtoErrorEnumTypeTransformer] instance. - static BulkIdResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart index 0b7f0738fe..796138b0bf 100644 --- a/mobile/openapi/lib/model/cast_response.dart +++ b/mobile/openapi/lib/model/cast_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class CastResponse { /// Returns a new [CastResponse] instance. CastResponse({ - this.gCastEnabled = false, + required this.gCastEnabled, }); /// Whether Google Cast is enabled diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart deleted file mode 100644 index 6e4a471092..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_dto.dart +++ /dev/null @@ -1,111 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class CheckExistingAssetsDto { - /// Returns a new [CheckExistingAssetsDto] instance. - CheckExistingAssetsDto({ - this.deviceAssetIds = const [], - required this.deviceId, - }); - - /// Device asset IDs to check - List deviceAssetIds; - - /// Device ID - String deviceId; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsDto && - _deepEquality.equals(other.deviceAssetIds, deviceAssetIds) && - other.deviceId == deviceId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (deviceAssetIds.hashCode) + - (deviceId.hashCode); - - @override - String toString() => 'CheckExistingAssetsDto[deviceAssetIds=$deviceAssetIds, deviceId=$deviceId]'; - - Map toJson() { - final json = {}; - json[r'deviceAssetIds'] = this.deviceAssetIds; - json[r'deviceId'] = this.deviceId; - return json; - } - - /// Returns a new [CheckExistingAssetsDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsDto( - deviceAssetIds: json[r'deviceAssetIds'] is Iterable - ? (json[r'deviceAssetIds'] as Iterable).cast().toList(growable: false) - : const [], - deviceId: mapValueOfType(json, r'deviceId')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CheckExistingAssetsDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = CheckExistingAssetsDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = CheckExistingAssetsDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'deviceAssetIds', - 'deviceId', - }; -} - diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart deleted file mode 100644 index 9fb13f100f..0000000000 --- a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart +++ /dev/null @@ -1,102 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class CheckExistingAssetsResponseDto { - /// Returns a new [CheckExistingAssetsResponseDto] instance. - CheckExistingAssetsResponseDto({ - this.existingIds = const [], - }); - - /// Existing asset IDs - List existingIds; - - @override - bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsResponseDto && - _deepEquality.equals(other.existingIds, existingIds); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (existingIds.hashCode); - - @override - String toString() => 'CheckExistingAssetsResponseDto[existingIds=$existingIds]'; - - Map toJson() { - final json = {}; - json[r'existingIds'] = this.existingIds; - return json; - } - - /// Returns a new [CheckExistingAssetsResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static CheckExistingAssetsResponseDto? fromJson(dynamic value) { - upgradeDto(value, "CheckExistingAssetsResponseDto"); - if (value is Map) { - final json = value.cast(); - - return CheckExistingAssetsResponseDto( - existingIds: json[r'existingIds'] is Iterable - ? (json[r'existingIds'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CheckExistingAssetsResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = CheckExistingAssetsResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of CheckExistingAssetsResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = CheckExistingAssetsResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'existingIds', - }; -} - diff --git a/mobile/openapi/lib/model/contributor_count_response_dto.dart b/mobile/openapi/lib/model/contributor_count_response_dto.dart index 1bef8f29d8..af5b2cbf68 100644 --- a/mobile/openapi/lib/model/contributor_count_response_dto.dart +++ b/mobile/openapi/lib/model/contributor_count_response_dto.dart @@ -18,6 +18,9 @@ class ContributorCountResponseDto { }); /// Number of assets contributed + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// User ID diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 69942fee5c..ba12c62d76 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -13,17 +13,17 @@ part of openapi.api; class CreateLibraryDto { /// Returns a new [CreateLibraryDto] instance. CreateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, required this.ownerId, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -57,8 +57,8 @@ class CreateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -78,11 +78,11 @@ class CreateLibraryDto { return CreateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, ); diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index 20d7cbd5e7..c6ec0d94a0 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -45,7 +45,9 @@ class CreateProfileImageResponseDto { Map toJson() { final json = {}; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; json[r'userId'] = this.userId; return json; @@ -60,7 +62,7 @@ class CreateProfileImageResponseDto { final json = value.cast(); return CreateProfileImageResponseDto( - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, userId: mapValueOfType(json, r'userId')!, ); diff --git a/mobile/openapi/lib/model/database_backup_delete_dto.dart b/mobile/openapi/lib/model/database_backup_delete_dto.dart index 8bc33a81dc..c336270b84 100644 --- a/mobile/openapi/lib/model/database_backup_delete_dto.dart +++ b/mobile/openapi/lib/model/database_backup_delete_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupDeleteDto { this.backups = const [], }); + /// Backup filenames to delete List backups; @override diff --git a/mobile/openapi/lib/model/database_backup_dto.dart b/mobile/openapi/lib/model/database_backup_dto.dart index 34912a55e0..abfa637157 100644 --- a/mobile/openapi/lib/model/database_backup_dto.dart +++ b/mobile/openapi/lib/model/database_backup_dto.dart @@ -18,10 +18,13 @@ class DatabaseBackupDto { required this.timezone, }); + /// Backup filename String filename; + /// Backup file size num filesize; + /// Backup timezone String timezone; @override diff --git a/mobile/openapi/lib/model/database_backup_list_response_dto.dart b/mobile/openapi/lib/model/database_backup_list_response_dto.dart index 16985dd605..de7bf78d5a 100644 --- a/mobile/openapi/lib/model/database_backup_list_response_dto.dart +++ b/mobile/openapi/lib/model/database_backup_list_response_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupListResponseDto { this.backups = const [], }); + /// List of backups List backups; @override diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart index 97a3346a67..dcb1258457 100644 --- a/mobile/openapi/lib/model/download_archive_info.dart +++ b/mobile/openapi/lib/model/download_archive_info.dart @@ -21,6 +21,9 @@ class DownloadArchiveInfo { List assetIds; /// Archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int size; @override diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart index a1ba44920e..8a0cebd945 100644 --- a/mobile/openapi/lib/model/download_info_dto.dart +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -31,6 +31,7 @@ class DownloadInfoDto { /// Archive size limit in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 32e9487475..bc1d7b4047 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -14,10 +14,13 @@ class DownloadResponse { /// Returns a new [DownloadResponse] instance. DownloadResponse({ required this.archiveSize, - this.includeEmbeddedVideos = false, + required this.includeEmbeddedVideos, }); /// Maximum archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int archiveSize; /// Whether to include embedded videos in downloads diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart index 81912e1d30..bfe32307fa 100644 --- a/mobile/openapi/lib/model/download_response_dto.dart +++ b/mobile/openapi/lib/model/download_response_dto.dart @@ -21,6 +21,9 @@ class DownloadResponseDto { List archives; /// Total size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int totalSize; @override diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 4acc1c8bd3..c5feb9df43 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -20,6 +20,7 @@ class DownloadUpdate { /// Maximum archive size in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 6bb58a8ab9..64a5a73bed 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -50,9 +50,13 @@ class ExifResponseDto { String? description; /// Image height in pixels + /// + /// Minimum value: 0 num? exifImageHeight; /// Image width in pixels + /// + /// Minimum value: 0 num? exifImageWidth; /// Exposure time @@ -62,6 +66,9 @@ class ExifResponseDto { num? fNumber; /// File size in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length in mm diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 4b9d7a6e9e..66cb542ccf 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -32,6 +32,7 @@ class FacialRecognitionConfig { /// Minimum number of faces required for recognition /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int minFaces; /// Minimum confidence score for face detection diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart index 906a95a83c..873404c786 100644 --- a/mobile/openapi/lib/model/folders_response.dart +++ b/mobile/openapi/lib/model/folders_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class FoldersResponse { /// Returns a new [FoldersResponse] instance. FoldersResponse({ - this.enabled = false, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether folders are enabled diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart index 3a3412384e..fe6743cba0 100644 --- a/mobile/openapi/lib/model/job_create_dto.dart +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -16,7 +16,6 @@ class JobCreateDto { required this.name, }); - /// Job name ManualJobName name; @override diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 96b9339b7d..08f70569f8 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -38,7 +38,6 @@ class JobName { static const assetFileMigration = JobName._(r'AssetFileMigration'); static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll'); static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails'); - static const auditLogCleanup = JobName._(r'AuditLogCleanup'); static const auditTableCleanup = JobName._(r'AuditTableCleanup'); static const databaseBackup = JobName._(r'DatabaseBackup'); static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll'); @@ -97,7 +96,6 @@ class JobName { assetFileMigration, assetGenerateThumbnailsQueueAll, assetGenerateThumbnails, - auditLogCleanup, auditTableCleanup, databaseBackup, facialRecognitionQueueAll, @@ -191,7 +189,6 @@ class JobNameTypeTransformer { case r'AssetFileMigration': return JobName.assetFileMigration; case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll; case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails; - case r'AuditLogCleanup': return JobName.auditLogCleanup; case r'AuditTableCleanup': return JobName.auditTableCleanup; case r'DatabaseBackup': return JobName.databaseBackup; case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll; diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart index 73a0187ddd..98fe3d3536 100644 --- a/mobile/openapi/lib/model/job_settings_dto.dart +++ b/mobile/openapi/lib/model/job_settings_dto.dart @@ -19,6 +19,7 @@ class JobSettingsDto { /// Concurrency /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int concurrency; @override diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index aa9158e591..88ebceae24 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -25,6 +25,9 @@ class LibraryResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assetCount; /// Creation date @@ -82,18 +85,24 @@ class LibraryResponseDto { Map toJson() { final json = {}; json[r'assetCount'] = this.assetCount; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'id'] = this.id; json[r'importPaths'] = this.importPaths; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; if (this.refreshedAt != null) { - json[r'refreshedAt'] = this.refreshedAt!.toUtc().toIso8601String(); + json[r'refreshedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.refreshedAt!.millisecondsSinceEpoch + : this.refreshedAt!.toUtc().toIso8601String(); } else { // json[r'refreshedAt'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -107,7 +116,7 @@ class LibraryResponseDto { return LibraryResponseDto( assetCount: mapValueOfType(json, r'assetCount')!, - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, exclusionPatterns: json[r'exclusionPatterns'] is Iterable ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) : const [], @@ -117,8 +126,8 @@ class LibraryResponseDto { : const [], name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - refreshedAt: mapDateTime(json, r'refreshedAt', r''), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + refreshedAt: mapDateTime(json, r'refreshedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index 6eec3ae8d7..55adbc2b49 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -13,22 +13,34 @@ part of openapi.api; class LibraryStatsResponseDto { /// Returns a new [LibraryStatsResponseDto] instance. LibraryStatsResponseDto({ - this.photos = 0, - this.total = 0, - this.usage = 0, - this.videos = 0, + required this.photos, + required this.total, + required this.usage, + required this.videos, }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart index ea1fee9d7a..d1818a2a43 100644 --- a/mobile/openapi/lib/model/license_key_dto.dart +++ b/mobile/openapi/lib/model/license_key_dto.dart @@ -20,7 +20,7 @@ class LicenseKeyDto { /// Activation key String activationKey; - /// License key (format: IM(SV|CL)(-XXXX){8}) + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart deleted file mode 100644 index 84ff72c1eb..0000000000 --- a/mobile/openapi/lib/model/license_response_dto.dart +++ /dev/null @@ -1,118 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class LicenseResponseDto { - /// Returns a new [LicenseResponseDto] instance. - LicenseResponseDto({ - required this.activatedAt, - required this.activationKey, - required this.licenseKey, - }); - - /// Activation date - DateTime activatedAt; - - /// Activation key - String activationKey; - - /// License key (format: IM(SV|CL)(-XXXX){8}) - String licenseKey; - - @override - bool operator ==(Object other) => identical(this, other) || other is LicenseResponseDto && - other.activatedAt == activatedAt && - other.activationKey == activationKey && - other.licenseKey == licenseKey; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (activatedAt.hashCode) + - (activationKey.hashCode) + - (licenseKey.hashCode); - - @override - String toString() => 'LicenseResponseDto[activatedAt=$activatedAt, activationKey=$activationKey, licenseKey=$licenseKey]'; - - Map toJson() { - final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); - json[r'activationKey'] = this.activationKey; - json[r'licenseKey'] = this.licenseKey; - return json; - } - - /// Returns a new [LicenseResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static LicenseResponseDto? fromJson(dynamic value) { - upgradeDto(value, "LicenseResponseDto"); - if (value is Map) { - final json = value.cast(); - - return LicenseResponseDto( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, - activationKey: mapValueOfType(json, r'activationKey')!, - licenseKey: mapValueOfType(json, r'licenseKey')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = LicenseResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = LicenseResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of LicenseResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = LicenseResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'activatedAt', - 'activationKey', - 'licenseKey', - }; -} - diff --git a/mobile/openapi/lib/model/log_level.dart b/mobile/openapi/lib/model/log_level.dart index 2129096da2..edb6a1ddda 100644 --- a/mobile/openapi/lib/model/log_level.dart +++ b/mobile/openapi/lib/model/log_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Log level class LogLevel { /// Instantiate a new enum with the provided [value]. const LogLevel._(this.value); diff --git a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart index ad524914b4..e3f8c0acbe 100644 --- a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart +++ b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart @@ -22,7 +22,6 @@ class MaintenanceDetectInstallStorageFolderDto { /// Number of files in the folder num files; - /// Storage folder StorageFolder folder; /// Whether the folder is readable diff --git a/mobile/openapi/lib/model/maintenance_status_response_dto.dart b/mobile/openapi/lib/model/maintenance_status_response_dto.dart index 52dbb5b95b..124fa674fd 100644 --- a/mobile/openapi/lib/model/maintenance_status_response_dto.dart +++ b/mobile/openapi/lib/model/maintenance_status_response_dto.dart @@ -20,7 +20,6 @@ class MaintenanceStatusResponseDto { this.task, }); - /// Maintenance action MaintenanceAction action; bool active; diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart index d09790a81a..27753eb9dc 100644 --- a/mobile/openapi/lib/model/manual_job_name.dart +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Job name +/// Manual job name class ManualJobName { /// Instantiate a new enum with the provided [value]. const ManualJobName._(this.value); diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index 63d4094cd0..250e214a60 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -13,11 +13,14 @@ part of openapi.api; class MemoriesResponse { /// Returns a new [MemoriesResponse] instance. MemoriesResponse({ - this.duration = 5, - this.enabled = true, + required this.duration, + required this.enabled, }); /// Memory duration in seconds + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int duration; /// Whether memories are enabled diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index d27cef022d..ede9910d74 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -20,6 +20,7 @@ class MemoriesUpdate { /// Memory duration in seconds /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 5b8eeed8fb..b906f6dd1d 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -67,7 +67,6 @@ class MemoryCreateDto { /// DateTime? showAt; - /// Memory type MemoryType type; @override @@ -101,7 +100,9 @@ class MemoryCreateDto { json[r'assetIds'] = this.assetIds; json[r'data'] = this.data; if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } @@ -110,14 +111,20 @@ class MemoryCreateDto { } else { // json[r'isSaved'] = null; } - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } @@ -138,11 +145,11 @@ class MemoryCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], data: OnThisDayDto.fromJson(json[r'data'])!, - hideAt: mapDateTime(json, r'hideAt', r''), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r'')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, ); } diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 1835095cf7..e736667d57 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -83,7 +83,6 @@ class MemoryResponseDto { /// DateTime? showAt; - /// Memory type MemoryType type; /// Last update date @@ -128,34 +127,48 @@ class MemoryResponseDto { Map toJson() { final json = {}; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -169,18 +182,18 @@ class MemoryResponseDto { return MemoryResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, data: OnThisDayDto.fromJson(json[r'data'])!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/memory_search_order.dart b/mobile/openapi/lib/model/memory_search_order.dart index bdf5b59894..67d0b69f46 100644 --- a/mobile/openapi/lib/model/memory_search_order.dart +++ b/mobile/openapi/lib/model/memory_search_order.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Sort order class MemorySearchOrder { /// Instantiate a new enum with the provided [value]. const MemorySearchOrder._(this.value); diff --git a/mobile/openapi/lib/model/memory_statistics_response_dto.dart b/mobile/openapi/lib/model/memory_statistics_response_dto.dart index bde78de481..ae542870d9 100644 --- a/mobile/openapi/lib/model/memory_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/memory_statistics_response_dto.dart @@ -17,6 +17,9 @@ class MemoryStatisticsResponseDto { }); /// Total number of memories + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/memory_type.dart b/mobile/openapi/lib/model/memory_type.dart index aee7bd1ba1..ecfc93edb0 100644 --- a/mobile/openapi/lib/model/memory_type.dart +++ b/mobile/openapi/lib/model/memory_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Memory type class MemoryType { /// Instantiate a new enum with the provided [value]. const MemoryType._(this.value); diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart index 4905b161bf..d8d7e9643b 100644 --- a/mobile/openapi/lib/model/memory_update_dto.dart +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -69,12 +69,16 @@ class MemoryUpdateDto { // json[r'isSaved'] = null; } if (this.memoryAt != null) { - json[r'memoryAt'] = this.memoryAt!.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt!.millisecondsSinceEpoch + : this.memoryAt!.toUtc().toIso8601String(); } else { // json[r'memoryAt'] = null; } if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } @@ -91,8 +95,8 @@ class MemoryUpdateDto { return MemoryUpdateDto( isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r''), - seenAt: mapDateTime(json, r'seenAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 4dbc90d407..d49ea7a4e5 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -20,8 +20,6 @@ class MetadataSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceAssetId, - this.deviceId, this.encodedVideoPath, this.id, this.isEncoded, @@ -34,7 +32,7 @@ class MetadataSearchDto { this.make, this.model, this.ocr, - this.order = AssetOrder.desc, + this.order, this.originalFileName, this.originalPath, this.page, @@ -104,24 +102,6 @@ class MetadataSearchDto { /// String? description; - /// Filter by device asset ID - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceAssetId; - - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded video file path /// /// Please note: This property should have been non-nullable! Since the specification file @@ -192,12 +172,6 @@ class MetadataSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,8 +186,13 @@ class MetadataSearchDto { /// String? ocr; - /// Sort order - AssetOrder order; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetOrder? order; /// Filter by original file name /// @@ -325,7 +304,6 @@ class MetadataSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -352,7 +330,6 @@ class MetadataSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -406,8 +383,6 @@ class MetadataSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && other.encodedVideoPath == encodedVideoPath && other.id == id && other.isEncoded == isEncoded && @@ -454,8 +429,6 @@ class MetadataSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceAssetId == null ? 0 : deviceAssetId!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + (id == null ? 0 : id!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + @@ -468,7 +441,7 @@ class MetadataSearchDto { (make == null ? 0 : make!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + - (order.hashCode) + + (order == null ? 0 : order!.hashCode) + (originalFileName == null ? 0 : originalFileName!.hashCode) + (originalPath == null ? 0 : originalPath!.hashCode) + (page == null ? 0 : page!.hashCode) + @@ -493,7 +466,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -514,12 +487,16 @@ class MetadataSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -528,16 +505,6 @@ class MetadataSearchDto { } else { // json[r'description'] = null; } - if (this.deviceAssetId != null) { - json[r'deviceAssetId'] = this.deviceAssetId; - } else { - // json[r'deviceAssetId'] = null; - } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.encodedVideoPath != null) { json[r'encodedVideoPath'] = this.encodedVideoPath; } else { @@ -598,7 +565,11 @@ class MetadataSearchDto { } else { // json[r'ocr'] = null; } + if (this.order != null) { json[r'order'] = this.order; + } else { + // json[r'order'] = null; + } if (this.originalFileName != null) { json[r'originalFileName'] = this.originalFileName; } else { @@ -641,12 +612,16 @@ class MetadataSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } @@ -656,12 +631,16 @@ class MetadataSearchDto { // json[r'thumbnailPath'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -671,12 +650,16 @@ class MetadataSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -723,11 +706,9 @@ class MetadataSearchDto { checksum: mapValueOfType(json, r'checksum'), city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), - deviceAssetId: mapValueOfType(json, r'deviceAssetId'), - deviceId: mapValueOfType(json, r'deviceId'), encodedVideoPath: mapValueOfType(json, r'encodedVideoPath'), id: mapValueOfType(json, r'id'), isEncoded: mapValueOfType(json, r'isEncoded'), @@ -740,7 +721,7 @@ class MetadataSearchDto { make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), - order: AssetOrder.fromJson(json[r'order']) ?? AssetOrder.desc, + order: AssetOrder.fromJson(json[r'order']), originalFileName: mapValueOfType(json, r'originalFileName'), originalPath: mapValueOfType(json, r'originalPath'), page: num.parse('${json[r'page']}'), @@ -756,14 +737,14 @@ class MetadataSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), thumbnailPath: mapValueOfType(json, r'thumbnailPath'), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/mirror_parameters.dart b/mobile/openapi/lib/model/mirror_parameters.dart index e8b8db685b..78c3da786c 100644 --- a/mobile/openapi/lib/model/mirror_parameters.dart +++ b/mobile/openapi/lib/model/mirror_parameters.dart @@ -16,7 +16,6 @@ class MirrorParameters { required this.axis, }); - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/notification_create_dto.dart b/mobile/openapi/lib/model/notification_create_dto.dart index 1288da8670..f9771246f9 100644 --- a/mobile/openapi/lib/model/notification_create_dto.dart +++ b/mobile/openapi/lib/model/notification_create_dto.dart @@ -13,7 +13,7 @@ part of openapi.api; class NotificationCreateDto { /// Returns a new [NotificationCreateDto] instance. NotificationCreateDto({ - this.data, + this.data = const {}, this.description, this.level, this.readAt, @@ -23,18 +23,11 @@ class NotificationCreateDto { }); /// Additional notification data - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? data; + Map data; /// Notification description String? description; - /// Notification level /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -49,7 +42,6 @@ class NotificationCreateDto { /// Notification title String title; - /// Notification type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -63,7 +55,7 @@ class NotificationCreateDto { @override bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.level == level && other.readAt == readAt && @@ -74,7 +66,7 @@ class NotificationCreateDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (level == null ? 0 : level!.hashCode) + (readAt == null ? 0 : readAt!.hashCode) + @@ -87,11 +79,7 @@ class NotificationCreateDto { Map toJson() { final json = {}; - if (this.data != null) { json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -103,7 +91,9 @@ class NotificationCreateDto { // json[r'level'] = null; } if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -126,10 +116,10 @@ class NotificationCreateDto { final json = value.cast(); return NotificationCreateDto( - data: mapValueOfType(json, r'data'), + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), level: NotificationLevel.fromJson(json[r'level']), - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type']), userId: mapValueOfType(json, r'userId')!, diff --git a/mobile/openapi/lib/model/notification_dto.dart b/mobile/openapi/lib/model/notification_dto.dart index 30d43de115..ad0e79cb27 100644 --- a/mobile/openapi/lib/model/notification_dto.dart +++ b/mobile/openapi/lib/model/notification_dto.dart @@ -14,7 +14,7 @@ class NotificationDto { /// Returns a new [NotificationDto] instance. NotificationDto({ required this.createdAt, - this.data, + this.data = const {}, this.description, required this.id, required this.level, @@ -27,13 +27,7 @@ class NotificationDto { DateTime createdAt; /// Additional notification data - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? data; + Map data; /// Notification description /// @@ -47,7 +41,6 @@ class NotificationDto { /// Notification ID String id; - /// Notification level NotificationLevel level; /// Date when notification was read @@ -62,13 +55,12 @@ class NotificationDto { /// Notification title String title; - /// Notification type NotificationType type; @override bool operator ==(Object other) => identical(this, other) || other is NotificationDto && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.id == id && other.level == level && @@ -80,7 +72,7 @@ class NotificationDto { int get hashCode => // ignore: unnecessary_parenthesis (createdAt.hashCode) + - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (id.hashCode) + (level.hashCode) + @@ -93,12 +85,10 @@ class NotificationDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - if (this.data != null) { + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -107,7 +97,9 @@ class NotificationDto { json[r'id'] = this.id; json[r'level'] = this.level; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -125,12 +117,12 @@ class NotificationDto { final json = value.cast(); return NotificationDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data'), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), id: mapValueOfType(json, r'id')!, level: NotificationLevel.fromJson(json[r'level'])!, - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type'])!, ); diff --git a/mobile/openapi/lib/model/notification_level.dart b/mobile/openapi/lib/model/notification_level.dart index 554863ae4f..4ca4e2bcc8 100644 --- a/mobile/openapi/lib/model/notification_level.dart +++ b/mobile/openapi/lib/model/notification_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification level class NotificationLevel { /// Instantiate a new enum with the provided [value]. const NotificationLevel._(this.value); diff --git a/mobile/openapi/lib/model/notification_type.dart b/mobile/openapi/lib/model/notification_type.dart index b5885aa441..dbc9c12f84 100644 --- a/mobile/openapi/lib/model/notification_type.dart +++ b/mobile/openapi/lib/model/notification_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification type class NotificationType { /// Instantiate a new enum with the provided [value]. const NotificationType._(this.value); diff --git a/mobile/openapi/lib/model/notification_update_all_dto.dart b/mobile/openapi/lib/model/notification_update_all_dto.dart index a157058324..5ac61ededc 100644 --- a/mobile/openapi/lib/model/notification_update_all_dto.dart +++ b/mobile/openapi/lib/model/notification_update_all_dto.dart @@ -41,7 +41,9 @@ class NotificationUpdateAllDto { final json = {}; json[r'ids'] = this.ids; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -60,7 +62,7 @@ class NotificationUpdateAllDto { ids: json[r'ids'] is Iterable ? (json[r'ids'] as Iterable).cast().toList(growable: false) : const [], - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/notification_update_dto.dart b/mobile/openapi/lib/model/notification_update_dto.dart index eddf9c7e12..c5d949d7b2 100644 --- a/mobile/openapi/lib/model/notification_update_dto.dart +++ b/mobile/openapi/lib/model/notification_update_dto.dart @@ -34,7 +34,9 @@ class NotificationUpdateDto { Map toJson() { final json = {}; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -50,7 +52,7 @@ class NotificationUpdateDto { final json = value.cast(); return NotificationUpdateDto( - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart index 77466d61d9..b63f027af7 100644 --- a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart +++ b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Token endpoint auth method +/// OAuth token endpoint auth method class OAuthTokenEndpointAuthMethod { /// Instantiate a new enum with the provided [value]. const OAuthTokenEndpointAuthMethod._(this.value); diff --git a/mobile/openapi/lib/model/ocr_config.dart b/mobile/openapi/lib/model/ocr_config.dart index d97cd5ffca..2ce5646731 100644 --- a/mobile/openapi/lib/model/ocr_config.dart +++ b/mobile/openapi/lib/model/ocr_config.dart @@ -26,6 +26,7 @@ class OcrConfig { /// Maximum resolution for OCR processing /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int maxResolution; /// Minimum confidence score for text detection diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index 93ec956f58..77ae96532f 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -18,8 +18,9 @@ class OnThisDayDto { /// Year for on this day memory /// - /// Minimum value: 1 - num year; + /// Minimum value: 1000 + /// Maximum value: 9999 + int year; @override bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto && @@ -48,7 +49,7 @@ class OnThisDayDto { final json = value.cast(); return OnThisDayDto( - year: num.parse('${json[r'year']}'), + year: mapValueOfType(json, r'year')!, ); } return null; diff --git a/mobile/openapi/lib/model/partner_direction.dart b/mobile/openapi/lib/model/partner_direction.dart index c43c0df75d..c5e3b308ac 100644 --- a/mobile/openapi/lib/model/partner_direction.dart +++ b/mobile/openapi/lib/model/partner_direction.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Partner direction class PartnerDirection { /// Instantiate a new enum with the provided [value]. const PartnerDirection._(this.value); diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 5789938d18..f4612cc98a 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -22,7 +22,6 @@ class PartnerResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index c09560e08c..9d5d8ec18a 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class PeopleResponse { /// Returns a new [PeopleResponse] instance. PeopleResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether people are enabled diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index f345657e73..87edc6b4a7 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -29,12 +29,17 @@ class PeopleResponseDto { bool? hasNextPage; /// Number of hidden people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int hidden; - /// List of people List people; /// Total number of people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 9092ede786..0ac9461027 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -41,7 +41,6 @@ class Permission { static const assetPeriodView = Permission._(r'asset.view'); static const assetPeriodDownload = Permission._(r'asset.download'); static const assetPeriodUpload = Permission._(r'asset.upload'); - static const assetPeriodReplace = Permission._(r'asset.replace'); static const assetPeriodCopy = Permission._(r'asset.copy'); static const assetPeriodDerive = Permission._(r'asset.derive'); static const assetPeriodEditPeriodGet = Permission._(r'asset.edit.get'); @@ -200,7 +199,6 @@ class Permission { assetPeriodView, assetPeriodDownload, assetPeriodUpload, - assetPeriodReplace, assetPeriodCopy, assetPeriodDerive, assetPeriodEditPeriodGet, @@ -394,7 +392,6 @@ class PermissionTypeTransformer { case r'asset.view': return Permission.assetPeriodView; case r'asset.download': return Permission.assetPeriodDownload; case r'asset.upload': return Permission.assetPeriodUpload; - case r'asset.replace': return Permission.assetPeriodReplace; case r'asset.copy': return Permission.assetPeriodCopy; case r'asset.derive': return Permission.assetPeriodDerive; case r'asset.edit.get': return Permission.assetPeriodEditPeriodGet; diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart index d2b45c8ccb..aeac16cc8a 100644 --- a/mobile/openapi/lib/model/person_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/person_statistics_response_dto.dart @@ -17,6 +17,9 @@ class PersonStatisticsResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assets; @override diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index f31c04b69f..f710dff8b9 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -36,7 +36,6 @@ class PersonWithFacesResponseDto { /// String? color; - /// Face detections List faces; /// Person ID diff --git a/mobile/openapi/lib/model/plugin_action_response_dto.dart b/mobile/openapi/lib/model/plugin_action_response_dto.dart index 34fa314ba9..cff2dc92f7 100644 --- a/mobile/openapi/lib/model/plugin_action_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_action_response_dto.dart @@ -35,7 +35,7 @@ class PluginActionResponseDto { String pluginId; /// Action schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginActionResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_context_type.dart b/mobile/openapi/lib/model/plugin_context_type.dart index 6f4ac91fdb..beda0b0f1a 100644 --- a/mobile/openapi/lib/model/plugin_context_type.dart +++ b/mobile/openapi/lib/model/plugin_context_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Context type +/// Plugin context class PluginContextType { /// Instantiate a new enum with the provided [value]. const PluginContextType._(this.value); diff --git a/mobile/openapi/lib/model/plugin_filter_response_dto.dart b/mobile/openapi/lib/model/plugin_filter_response_dto.dart index ea6411a9c1..d1ab867ff9 100644 --- a/mobile/openapi/lib/model/plugin_filter_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_filter_response_dto.dart @@ -35,7 +35,7 @@ class PluginFilterResponseDto { String pluginId; /// Filter schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginFilterResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_json_schema.dart b/mobile/openapi/lib/model/plugin_json_schema.dart new file mode 100644 index 0000000000..f7a2d584d9 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema.dart @@ -0,0 +1,158 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PluginJsonSchema { + /// Returns a new [PluginJsonSchema] instance. + PluginJsonSchema({ + this.additionalProperties, + this.description, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? additionalProperties; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + Map properties; + + List required_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchema && + other.additionalProperties == additionalProperties && + other.description == description && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchema[additionalProperties=$additionalProperties, description=$description, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchema] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchema? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchema"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchema( + additionalProperties: mapValueOfType(json, r'additionalProperties'), + description: mapValueOfType(json, r'description'), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchema.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchema.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchema-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchema.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property.dart b/mobile/openapi/lib/model/plugin_json_schema_property.dart new file mode 100644 index 0000000000..65951da0a3 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property.dart @@ -0,0 +1,195 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PluginJsonSchemaProperty { + /// Returns a new [PluginJsonSchemaProperty] instance. + PluginJsonSchemaProperty({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + List enum_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaProperty && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaProperty[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaProperty] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaProperty? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaProperty"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaProperty( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaProperty.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchemaProperty.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaProperty-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchemaProperty.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart new file mode 100644 index 0000000000..169c6be772 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart @@ -0,0 +1,195 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PluginJsonSchemaPropertyAdditionalProperties { + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance. + PluginJsonSchemaPropertyAdditionalProperties({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + List enum_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaPropertyAdditionalProperties && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaPropertyAdditionalProperties[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaPropertyAdditionalProperties? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaPropertyAdditionalProperties"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaPropertyAdditionalProperties( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaPropertyAdditionalProperties-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchemaPropertyAdditionalProperties.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_type.dart b/mobile/openapi/lib/model/plugin_json_schema_type.dart new file mode 100644 index 0000000000..cabac9b71b --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_type.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class PluginJsonSchemaType { + /// Instantiate a new enum with the provided [value]. + const PluginJsonSchemaType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const string = PluginJsonSchemaType._(r'string'); + static const number = PluginJsonSchemaType._(r'number'); + static const integer = PluginJsonSchemaType._(r'integer'); + static const boolean = PluginJsonSchemaType._(r'boolean'); + static const object = PluginJsonSchemaType._(r'object'); + static const array = PluginJsonSchemaType._(r'array'); + static const null_ = PluginJsonSchemaType._(r'null'); + + /// List of all possible values in this [enum][PluginJsonSchemaType]. + static const values = [ + string, + number, + integer, + boolean, + object, + array, + null_, + ]; + + static PluginJsonSchemaType? fromJson(dynamic value) => PluginJsonSchemaTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [PluginJsonSchemaType] to String, +/// and [decode] dynamic data back to [PluginJsonSchemaType]. +class PluginJsonSchemaTypeTypeTransformer { + factory PluginJsonSchemaTypeTypeTransformer() => _instance ??= const PluginJsonSchemaTypeTypeTransformer._(); + + const PluginJsonSchemaTypeTypeTransformer._(); + + String encode(PluginJsonSchemaType data) => data.value; + + /// Decodes a [dynamic value][data] to a PluginJsonSchemaType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + PluginJsonSchemaType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'string': return PluginJsonSchemaType.string; + case r'number': return PluginJsonSchemaType.number; + case r'integer': return PluginJsonSchemaType.integer; + case r'boolean': return PluginJsonSchemaType.boolean; + case r'object': return PluginJsonSchemaType.object; + case r'array': return PluginJsonSchemaType.array; + case r'null': return PluginJsonSchemaType.null_; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [PluginJsonSchemaTypeTypeTransformer] instance. + static PluginJsonSchemaTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart index 16a9604bcd..a6ee1c6b69 100644 --- a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -17,10 +17,8 @@ class PluginTriggerResponseDto { required this.type, }); - /// Context type PluginContextType contextType; - /// Trigger type PluginTriggerType type; @override diff --git a/mobile/openapi/lib/model/plugin_trigger_type.dart b/mobile/openapi/lib/model/plugin_trigger_type.dart index 9ae64acf6c..3ebcef7a95 100644 --- a/mobile/openapi/lib/model/plugin_trigger_type.dart +++ b/mobile/openapi/lib/model/plugin_trigger_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Trigger type +/// Plugin trigger type class PluginTriggerType { /// Instantiate a new enum with the provided [value]. const PluginTriggerType._(this.value); diff --git a/mobile/openapi/lib/model/queue_command_dto.dart b/mobile/openapi/lib/model/queue_command_dto.dart index 9e1eea15db..fb68d85583 100644 --- a/mobile/openapi/lib/model/queue_command_dto.dart +++ b/mobile/openapi/lib/model/queue_command_dto.dart @@ -17,7 +17,6 @@ class QueueCommandDto { this.force, }); - /// Queue command to execute QueueCommand command; /// Force the command execution (if applicable) diff --git a/mobile/openapi/lib/model/queue_job_response_dto.dart b/mobile/openapi/lib/model/queue_job_response_dto.dart index 2ce63784eb..06d433edad 100644 --- a/mobile/openapi/lib/model/queue_job_response_dto.dart +++ b/mobile/openapi/lib/model/queue_job_response_dto.dart @@ -13,14 +13,14 @@ part of openapi.api; class QueueJobResponseDto { /// Returns a new [QueueJobResponseDto] instance. QueueJobResponseDto({ - required this.data, + this.data = const {}, this.id, required this.name, required this.timestamp, }); /// Job data payload - Object data; + Map data; /// Job ID /// @@ -31,15 +31,17 @@ class QueueJobResponseDto { /// String? id; - /// Job name JobName name; /// Job creation timestamp + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int timestamp; @override bool operator ==(Object other) => identical(this, other) || other is QueueJobResponseDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.id == id && other.name == name && other.timestamp == timestamp; @@ -77,7 +79,7 @@ class QueueJobResponseDto { final json = value.cast(); return QueueJobResponseDto( - data: mapValueOfType(json, r'data')!, + data: mapCastOfType(json, r'data')!, id: mapValueOfType(json, r'id'), name: JobName.fromJson(json[r'name'])!, timestamp: mapValueOfType(json, r'timestamp')!, diff --git a/mobile/openapi/lib/model/queue_job_status.dart b/mobile/openapi/lib/model/queue_job_status.dart index 03a1371cc5..cbd01b11ed 100644 --- a/mobile/openapi/lib/model/queue_job_status.dart +++ b/mobile/openapi/lib/model/queue_job_status.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue job status class QueueJobStatus { /// Instantiate a new enum with the provided [value]. const QueueJobStatus._(this.value); diff --git a/mobile/openapi/lib/model/queue_name.dart b/mobile/openapi/lib/model/queue_name.dart index d94304d0d3..eb19d8957f 100644 --- a/mobile/openapi/lib/model/queue_name.dart +++ b/mobile/openapi/lib/model/queue_name.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue name class QueueName { /// Instantiate a new enum with the provided [value]. const QueueName._(this.value); diff --git a/mobile/openapi/lib/model/queue_response_dto.dart b/mobile/openapi/lib/model/queue_response_dto.dart index ac9244514c..c88f9fc195 100644 --- a/mobile/openapi/lib/model/queue_response_dto.dart +++ b/mobile/openapi/lib/model/queue_response_dto.dart @@ -21,7 +21,6 @@ class QueueResponseDto { /// Whether the queue is paused bool isPaused; - /// Queue name QueueName name; QueueStatisticsDto statistics; diff --git a/mobile/openapi/lib/model/queue_statistics_dto.dart b/mobile/openapi/lib/model/queue_statistics_dto.dart index c9a37ee30a..86c75f8e7c 100644 --- a/mobile/openapi/lib/model/queue_statistics_dto.dart +++ b/mobile/openapi/lib/model/queue_statistics_dto.dart @@ -22,21 +22,39 @@ class QueueStatisticsDto { }); /// Number of active jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int active; /// Number of completed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int completed; /// Number of delayed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int delayed; /// Number of failed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int failed; /// Number of paused jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int paused; /// Number of waiting jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int waiting; @override diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index d5803c9cc7..3f33d8f850 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -18,7 +18,6 @@ class RandomSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -75,15 +74,6 @@ class RandomSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -136,12 +126,6 @@ class RandomSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -219,7 +203,6 @@ class RandomSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -246,7 +229,6 @@ class RandomSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -298,7 +280,6 @@ class RandomSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -335,7 +316,6 @@ class RandomSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -365,7 +345,7 @@ class RandomSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -381,20 +361,19 @@ class RandomSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -467,22 +446,30 @@ class RandomSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -492,12 +479,16 @@ class RandomSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -543,9 +534,8 @@ class RandomSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), - deviceId: mapValueOfType(json, r'deviceId'), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -567,13 +557,13 @@ class RandomSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart index 4346fa5c58..7b067412bf 100644 --- a/mobile/openapi/lib/model/ratings_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class RatingsResponse { /// Returns a new [RatingsResponse] instance. RatingsResponse({ - this.enabled = false, + required this.enabled, }); /// Whether ratings are enabled diff --git a/mobile/openapi/lib/model/reaction_level.dart b/mobile/openapi/lib/model/reaction_level.dart index 29568b9d11..6060f4c2b7 100644 --- a/mobile/openapi/lib/model/reaction_level.dart +++ b/mobile/openapi/lib/model/reaction_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction level class ReactionLevel { /// Instantiate a new enum with the provided [value]. const ReactionLevel._(this.value); diff --git a/mobile/openapi/lib/model/reaction_type.dart b/mobile/openapi/lib/model/reaction_type.dart index 4c788138fb..c4daccad71 100644 --- a/mobile/openapi/lib/model/reaction_type.dart +++ b/mobile/openapi/lib/model/reaction_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction type class ReactionType { /// Instantiate a new enum with the provided [value]. const ReactionType._(this.value); diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index 8841251e4a..c21113ee6d 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -20,6 +20,9 @@ class SearchAlbumResponseDto { }); /// Number of albums in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -27,6 +30,9 @@ class SearchAlbumResponseDto { List items; /// Total number of matching albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index acb81f28e2..f4ffade26b 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -21,6 +21,9 @@ class SearchAssetResponseDto { }); /// Number of assets in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -31,6 +34,9 @@ class SearchAssetResponseDto { String? nextPage; /// Total number of matching assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index 8318fbfb3b..62adfaa74a 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -18,6 +18,9 @@ class SearchFacetCountResponseDto { }); /// Number of assets with this facet value + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; /// Facet value diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index 43b5ac5c81..51124ef1cf 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -17,7 +17,6 @@ class SearchFacetResponseDto { required this.fieldName, }); - /// Facet counts List counts; /// Facet field name diff --git a/mobile/openapi/lib/model/search_statistics_response_dto.dart b/mobile/openapi/lib/model/search_statistics_response_dto.dart index 5aebe4d6a9..c4d893af05 100644 --- a/mobile/openapi/lib/model/search_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/search_statistics_response_dto.dart @@ -17,6 +17,9 @@ class SearchStatisticsResponseDto { }); /// Total number of matching assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_suggestion_type.dart b/mobile/openapi/lib/model/search_suggestion_type.dart index b18fe687c4..6d44b881bd 100644 --- a/mobile/openapi/lib/model/search_suggestion_type.dart +++ b/mobile/openapi/lib/model/search_suggestion_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Suggestion type class SearchSuggestionType { /// Instantiate a new enum with the provided [value]. const SearchSuggestionType._(this.value); diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index fec096d51a..316edb609f 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -54,9 +54,15 @@ class ServerConfigDto { bool publicUsers; /// Number of days before trashed assets are permanently deleted + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int trashDays; /// Delay in days before deleted users are permanently removed + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int userDeleteDelay; @override diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index ef2fa458e2..605bd74f41 100644 --- a/mobile/openapi/lib/model/server_stats_response_dto.dart +++ b/mobile/openapi/lib/model/server_stats_response_dto.dart @@ -13,29 +13,45 @@ part of openapi.api; class ServerStatsResponseDto { /// Returns a new [ServerStatsResponseDto] instance. ServerStatsResponseDto({ - this.photos = 0, - this.usage = 0, + required this.photos, + required this.usage, this.usageByUser = const [], - this.usagePhotos = 0, - this.usageVideos = 0, - this.videos = 0, + required this.usagePhotos, + required this.usageVideos, + required this.videos, }); /// Total number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; + /// Array of usage for each user List usageByUser; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// Total number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart index 476b048b4d..4a66d54e37 100644 --- a/mobile/openapi/lib/model/server_storage_response_dto.dart +++ b/mobile/openapi/lib/model/server_storage_response_dto.dart @@ -26,12 +26,18 @@ class ServerStorageResponseDto { String diskAvailable; /// Available disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskAvailableRaw; /// Total disk size (human-readable format) String diskSize; /// Total disk size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskSizeRaw; /// Disk usage percentage (0-100) @@ -41,6 +47,9 @@ class ServerStorageResponseDto { String diskUse; /// Used disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskUseRaw; @override diff --git a/mobile/openapi/lib/model/server_theme_dto.dart b/mobile/openapi/lib/model/server_theme_dto.dart deleted file mode 100644 index 957cf84d55..0000000000 --- a/mobile/openapi/lib/model/server_theme_dto.dart +++ /dev/null @@ -1,100 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ServerThemeDto { - /// Returns a new [ServerThemeDto] instance. - ServerThemeDto({ - required this.customCss, - }); - - /// Custom CSS for theming - String customCss; - - @override - bool operator ==(Object other) => identical(this, other) || other is ServerThemeDto && - other.customCss == customCss; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (customCss.hashCode); - - @override - String toString() => 'ServerThemeDto[customCss=$customCss]'; - - Map toJson() { - final json = {}; - json[r'customCss'] = this.customCss; - return json; - } - - /// Returns a new [ServerThemeDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static ServerThemeDto? fromJson(dynamic value) { - upgradeDto(value, "ServerThemeDto"); - if (value is Map) { - final json = value.cast(); - - return ServerThemeDto( - customCss: mapValueOfType(json, r'customCss')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ServerThemeDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = ServerThemeDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of ServerThemeDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = ServerThemeDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'customCss', - }; -} - diff --git a/mobile/openapi/lib/model/server_version_history_response_dto.dart b/mobile/openapi/lib/model/server_version_history_response_dto.dart index c3b7049016..ae5e060cff 100644 --- a/mobile/openapi/lib/model/server_version_history_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_history_response_dto.dart @@ -45,7 +45,9 @@ class ServerVersionHistoryResponseDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'version'] = this.version; return json; @@ -60,7 +62,7 @@ class ServerVersionHistoryResponseDto { final json = value.cast(); return ServerVersionHistoryResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, version: mapValueOfType(json, r'version')!, ); diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index a13cd81ad7..60161a7458 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -19,12 +19,21 @@ class ServerVersionResponseDto { }); /// Major version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int major; /// Minor version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int minor; /// Patch version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int patch_; @override diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart index 14bf584bb9..e7c9dc0d63 100644 --- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart +++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart @@ -17,7 +17,6 @@ class SetMaintenanceModeDto { this.restoreBackupFilename, }); - /// Maintenance action MaintenanceAction action; /// Restore backup filename diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 2675ad4beb..a32714d556 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -64,7 +64,6 @@ class SharedLinkCreateDto { /// Custom URL slug String? slug; - /// Shared link type SharedLinkType type; @override @@ -117,7 +116,9 @@ class SharedLinkCreateDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -152,7 +153,7 @@ class SharedLinkCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata') ?? true, slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index b22232add6..11d6cdd52e 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -120,7 +120,9 @@ class SharedLinkEditDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -155,7 +157,7 @@ class SharedLinkEditDto { allowUpload: mapValueOfType(json, r'allowUpload'), changeExpiryTime: mapValueOfType(json, r'changeExpiryTime'), description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata'), slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index d9aec48c39..bad0966ca2 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -25,7 +25,6 @@ class SharedLinkResponseDto { required this.password, required this.showMetadata, required this.slug, - this.token, required this.type, required this.userId, }); @@ -70,10 +69,6 @@ class SharedLinkResponseDto { /// Custom URL slug String? slug; - /// Access token - String? token; - - /// Shared link type SharedLinkType type; /// Owner user ID @@ -93,7 +88,6 @@ class SharedLinkResponseDto { other.password == password && other.showMetadata == showMetadata && other.slug == slug && - other.token == token && other.type == type && other.userId == userId; @@ -112,12 +106,11 @@ class SharedLinkResponseDto { (password == null ? 0 : password!.hashCode) + (showMetadata.hashCode) + (slug == null ? 0 : slug!.hashCode) + - (token == null ? 0 : token!.hashCode) + (type.hashCode) + (userId.hashCode); @override - String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, slug=$slug, token=$token, type=$type, userId=$userId]'; + String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, slug=$slug, type=$type, userId=$userId]'; Map toJson() { final json = {}; @@ -129,14 +122,18 @@ class SharedLinkResponseDto { json[r'allowDownload'] = this.allowDownload; json[r'allowUpload'] = this.allowUpload; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.description != null) { json[r'description'] = this.description; } else { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -152,11 +149,6 @@ class SharedLinkResponseDto { json[r'slug'] = this.slug; } else { // json[r'slug'] = null; - } - if (this.token != null) { - json[r'token'] = this.token; - } else { - // json[r'token'] = null; } json[r'type'] = this.type; json[r'userId'] = this.userId; @@ -176,15 +168,14 @@ class SharedLinkResponseDto { allowDownload: mapValueOfType(json, r'allowDownload')!, allowUpload: mapValueOfType(json, r'allowUpload')!, assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, key: mapValueOfType(json, r'key')!, password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata')!, slug: mapValueOfType(json, r'slug'), - token: mapValueOfType(json, r'token'), type: SharedLinkType.fromJson(json[r'type'])!, userId: mapValueOfType(json, r'userId')!, ); diff --git a/mobile/openapi/lib/model/shared_links_response.dart b/mobile/openapi/lib/model/shared_links_response.dart index 510e94e43f..2b32a57540 100644 --- a/mobile/openapi/lib/model/shared_links_response.dart +++ b/mobile/openapi/lib/model/shared_links_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class SharedLinksResponse { /// Returns a new [SharedLinksResponse] instance. SharedLinksResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether shared links are enabled diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 5f8214467f..bf1465223e 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -18,7 +18,6 @@ class SmartSearchDto { this.country, this.createdAfter, this.createdBefore, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -77,15 +76,6 @@ class SmartSearchDto { /// DateTime? createdBefore; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -147,12 +137,6 @@ class SmartSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -259,7 +243,6 @@ class SmartSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -286,7 +269,6 @@ class SmartSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -320,7 +302,6 @@ class SmartSearchDto { other.country == country && other.createdAfter == createdAfter && other.createdBefore == createdBefore && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -359,7 +340,6 @@ class SmartSearchDto { (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -391,7 +371,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; @@ -407,20 +387,19 @@ class SmartSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -513,22 +492,30 @@ class SmartSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -538,12 +525,16 @@ class SmartSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -579,9 +570,8 @@ class SmartSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), - deviceId: mapValueOfType(json, r'deviceId'), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -607,13 +597,13 @@ class SmartSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index 638dfb5255..326f83a03d 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -18,7 +18,6 @@ class StackResponseDto { required this.primaryAssetId, }); - /// Stack assets List assets; /// Stack ID diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index d5bbf448a3..d0070e8e12 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -19,7 +19,6 @@ class StatisticsSearchDto { this.createdAfter, this.createdBefore, this.description, - this.deviceId, this.isEncoded, this.isFavorite, this.isMotion, @@ -80,15 +79,6 @@ class StatisticsSearchDto { /// String? description; - /// Device ID to filter by - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? deviceId; - /// Filter by encoded status /// /// Please note: This property should have been non-nullable! Since the specification file @@ -141,12 +131,6 @@ class StatisticsSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,7 +196,6 @@ class StatisticsSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -239,7 +222,6 @@ class StatisticsSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -256,7 +238,6 @@ class StatisticsSearchDto { other.createdAfter == createdAfter && other.createdBefore == createdBefore && other.description == description && - other.deviceId == deviceId && other.isEncoded == isEncoded && other.isFavorite == isFavorite && other.isMotion == isMotion && @@ -289,7 +270,6 @@ class StatisticsSearchDto { (createdAfter == null ? 0 : createdAfter!.hashCode) + (createdBefore == null ? 0 : createdBefore!.hashCode) + (description == null ? 0 : description!.hashCode) + - (deviceId == null ? 0 : deviceId!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + @@ -314,7 +294,7 @@ class StatisticsSearchDto { (visibility == null ? 0 : visibility!.hashCode); @override - String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; + String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; Map toJson() { final json = {}; @@ -330,12 +310,16 @@ class StatisticsSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -344,11 +328,6 @@ class StatisticsSearchDto { } else { // json[r'description'] = null; } - if (this.deviceId != null) { - json[r'deviceId'] = this.deviceId; - } else { - // json[r'deviceId'] = null; - } if (this.isEncoded != null) { json[r'isEncoded'] = this.isEncoded; } else { @@ -416,22 +395,30 @@ class StatisticsSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -441,12 +428,16 @@ class StatisticsSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -472,10 +463,9 @@ class StatisticsSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), - deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), @@ -496,13 +486,13 @@ class StatisticsSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/sync_ack_dto.dart b/mobile/openapi/lib/model/sync_ack_dto.dart index 747f671557..fa7e20a832 100644 --- a/mobile/openapi/lib/model/sync_ack_dto.dart +++ b/mobile/openapi/lib/model/sync_ack_dto.dart @@ -20,7 +20,6 @@ class SyncAckDto { /// Acknowledgment ID String ack; - /// Sync entity type SyncEntityType type; @override diff --git a/mobile/openapi/lib/model/sync_album_user_v1.dart b/mobile/openapi/lib/model/sync_album_user_v1.dart index 3fc8972069..1efe7da029 100644 --- a/mobile/openapi/lib/model/sync_album_user_v1.dart +++ b/mobile/openapi/lib/model/sync_album_user_v1.dart @@ -21,7 +21,6 @@ class SyncAlbumUserV1 { /// Album ID String albumId; - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/sync_album_v1.dart b/mobile/openapi/lib/model/sync_album_v1.dart index 6c89d93724..17b2bda02b 100644 --- a/mobile/openapi/lib/model/sync_album_v1.dart +++ b/mobile/openapi/lib/model/sync_album_v1.dart @@ -80,7 +80,9 @@ class SyncAlbumV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; json[r'id'] = this.id; json[r'isActivityEnabled'] = this.isActivityEnabled; @@ -92,7 +94,9 @@ class SyncAlbumV1 { } else { // json[r'thumbnailAssetId'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -105,7 +109,7 @@ class SyncAlbumV1 { final json = value.cast(); return SyncAlbumV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, description: mapValueOfType(json, r'description')!, id: mapValueOfType(json, r'id')!, isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, @@ -113,7 +117,7 @@ class SyncAlbumV1 { order: AssetOrder.fromJson(json[r'order'])!, ownerId: mapValueOfType(json, r'ownerId')!, thumbnailAssetId: mapValueOfType(json, r'thumbnailAssetId'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart index 68af280290..e0c98bfef3 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart @@ -16,6 +16,7 @@ class SyncAssetEditDeleteV1 { required this.editId, }); + /// Edit ID String editId; @override diff --git a/mobile/openapi/lib/model/sync_asset_edit_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_v1.dart index 3cc2673bfc..8acfad5f6a 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_v1.dart @@ -16,18 +16,25 @@ class SyncAssetEditV1 { required this.action, required this.assetId, required this.id, - required this.parameters, + this.parameters = const {}, required this.sequence, }); AssetEditAction action; + /// Asset ID String assetId; + /// Edit ID String id; - Object parameters; + /// Edit parameters + Map parameters; + /// Edit sequence + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int sequence; @override @@ -35,7 +42,7 @@ class SyncAssetEditV1 { other.action == action && other.assetId == assetId && other.id == id && - other.parameters == parameters && + _deepEquality.equals(other.parameters, parameters) && other.sequence == sequence; @override @@ -72,7 +79,7 @@ class SyncAssetEditV1 { action: AssetEditAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId')!, id: mapValueOfType(json, r'id')!, - parameters: mapValueOfType(json, r'parameters')!, + parameters: mapCastOfType(json, r'parameters')!, sequence: mapValueOfType(json, r'sequence')!, ); } diff --git a/mobile/openapi/lib/model/sync_asset_exif_v1.dart b/mobile/openapi/lib/model/sync_asset_exif_v1.dart index ff9efdfea3..caaeed7fb3 100644 --- a/mobile/openapi/lib/model/sync_asset_exif_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_exif_v1.dart @@ -56,9 +56,15 @@ class SyncAssetExifV1 { String? description; /// Exif image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageHeight; /// Exif image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageWidth; /// Exposure time @@ -68,6 +74,9 @@ class SyncAssetExifV1 { double? fNumber; /// File size in byte + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length @@ -77,6 +86,9 @@ class SyncAssetExifV1 { double? fps; /// ISO + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? iso; /// Latitude @@ -107,6 +119,9 @@ class SyncAssetExifV1 { String? projectionType; /// Rating + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? rating; /// State @@ -189,7 +204,9 @@ class SyncAssetExifV1 { // json[r'country'] = null; } if (this.dateTimeOriginal != null) { - json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String(); + json[r'dateTimeOriginal'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.dateTimeOriginal!.millisecondsSinceEpoch + : this.dateTimeOriginal!.toUtc().toIso8601String(); } else { // json[r'dateTimeOriginal'] = null; } @@ -264,7 +281,9 @@ class SyncAssetExifV1 { // json[r'model'] = null; } if (this.modifyDate != null) { - json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String(); + json[r'modifyDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.modifyDate!.millisecondsSinceEpoch + : this.modifyDate!.toUtc().toIso8601String(); } else { // json[r'modifyDate'] = null; } @@ -313,7 +332,7 @@ class SyncAssetExifV1 { assetId: mapValueOfType(json, r'assetId')!, city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''), + dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), exifImageHeight: mapValueOfType(json, r'exifImageHeight'), exifImageWidth: mapValueOfType(json, r'exifImageWidth'), @@ -328,7 +347,7 @@ class SyncAssetExifV1 { longitude: (mapValueOfType(json, r'longitude'))?.toDouble(), make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), - modifyDate: mapDateTime(json, r'modifyDate', r''), + modifyDate: mapDateTime(json, r'modifyDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), orientation: mapValueOfType(json, r'orientation'), profileDescription: mapValueOfType(json, r'profileDescription'), projectionType: mapValueOfType(json, r'projectionType'), diff --git a/mobile/openapi/lib/model/sync_asset_face_v1.dart b/mobile/openapi/lib/model/sync_asset_face_v1.dart index 647a07d5eb..c3f74ff2cd 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v1.dart @@ -28,19 +28,43 @@ class SyncAssetFaceV1 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID diff --git a/mobile/openapi/lib/model/sync_asset_face_v2.dart b/mobile/openapi/lib/model/sync_asset_face_v2.dart index 688d71229f..aeefc2ece9 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v2.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v2.dart @@ -30,12 +30,28 @@ class SyncAssetFaceV2 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face deleted at @@ -44,8 +60,16 @@ class SyncAssetFaceV2 { /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Is the face visible in the asset @@ -99,7 +123,9 @@ class SyncAssetFaceV2 { json[r'boundingBoxY1'] = this.boundingBoxY1; json[r'boundingBoxY2'] = this.boundingBoxY2; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -130,7 +156,7 @@ class SyncAssetFaceV2 { boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, diff --git a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart index 4a66623939..08d7eae49b 100644 --- a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart @@ -15,7 +15,7 @@ class SyncAssetMetadataV1 { SyncAssetMetadataV1({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class SyncAssetMetadataV1 { String key; /// Value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataV1 && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class SyncAssetMetadataV1 { return SyncAssetMetadataV1( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index debde4488e..d08de6ab72 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -50,6 +50,9 @@ class SyncAssetV1 { DateTime? fileModifiedAt; /// Asset height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? height; /// Asset ID @@ -82,13 +85,14 @@ class SyncAssetV1 { /// Thumbhash String? thumbhash; - /// Asset type AssetTypeEnum type; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? width; @override @@ -143,7 +147,9 @@ class SyncAssetV1 { final json = {}; json[r'checksum'] = this.checksum; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -153,12 +159,16 @@ class SyncAssetV1 { // json[r'duration'] = null; } if (this.fileCreatedAt != null) { - json[r'fileCreatedAt'] = this.fileCreatedAt!.toUtc().toIso8601String(); + json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.fileCreatedAt!.millisecondsSinceEpoch + : this.fileCreatedAt!.toUtc().toIso8601String(); } else { // json[r'fileCreatedAt'] = null; } if (this.fileModifiedAt != null) { - json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String(); + json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.fileModifiedAt!.millisecondsSinceEpoch + : this.fileModifiedAt!.toUtc().toIso8601String(); } else { // json[r'fileModifiedAt'] = null; } @@ -181,7 +191,9 @@ class SyncAssetV1 { // json[r'livePhotoVideoId'] = null; } if (this.localDateTime != null) { - json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); + json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.localDateTime!.millisecondsSinceEpoch + : this.localDateTime!.toUtc().toIso8601String(); } else { // json[r'localDateTime'] = null; } @@ -217,17 +229,17 @@ class SyncAssetV1 { return SyncAssetV1( checksum: mapValueOfType(json, r'checksum')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), duration: mapValueOfType(json, r'duration'), - fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''), - fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), + fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), height: mapValueOfType(json, r'height'), id: mapValueOfType(json, r'id')!, isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, libraryId: mapValueOfType(json, r'libraryId'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), - localDateTime: mapDateTime(json, r'localDateTime', r''), + localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, stackId: mapValueOfType(json, r'stackId'), diff --git a/mobile/openapi/lib/model/sync_auth_user_v1.dart b/mobile/openapi/lib/model/sync_auth_user_v1.dart index 0edd804c6a..c64d82bfbd 100644 --- a/mobile/openapi/lib/model/sync_auth_user_v1.dart +++ b/mobile/openapi/lib/model/sync_auth_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncAuthUserV1 { /// Returns a new [SyncAuthUserV1] instance. SyncAuthUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -28,7 +28,6 @@ class SyncAuthUserV1 { required this.storageLabel, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -58,8 +57,16 @@ class SyncAuthUserV1 { /// User profile changed at DateTime profileChangedAt; + /// Quota size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; + /// Quota usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int quotaUsageInBytes; /// User storage label @@ -109,7 +116,9 @@ class SyncAuthUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -124,7 +133,9 @@ class SyncAuthUserV1 { } else { // json[r'pinCode'] = null; } - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; } else { @@ -149,7 +160,7 @@ class SyncAuthUserV1 { return SyncAuthUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, @@ -157,7 +168,7 @@ class SyncAuthUserV1 { name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, pinCode: mapValueOfType(json, r'pinCode'), - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, storageLabel: mapValueOfType(json, r'storageLabel'), @@ -208,7 +219,6 @@ class SyncAuthUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/sync_memory_v1.dart b/mobile/openapi/lib/model/sync_memory_v1.dart index c506738d97..855340f4d7 100644 --- a/mobile/openapi/lib/model/sync_memory_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_v1.dart @@ -14,7 +14,7 @@ class SyncMemoryV1 { /// Returns a new [SyncMemoryV1] instance. SyncMemoryV1({ required this.createdAt, - required this.data, + this.data = const {}, required this.deletedAt, required this.hideAt, required this.id, @@ -31,7 +31,7 @@ class SyncMemoryV1 { DateTime createdAt; /// Data - Object data; + Map data; /// Deleted at DateTime? deletedAt; @@ -57,7 +57,6 @@ class SyncMemoryV1 { /// Show at DateTime? showAt; - /// Memory type MemoryType type; /// Updated at @@ -66,7 +65,7 @@ class SyncMemoryV1 { @override bool operator ==(Object other) => identical(this, other) || other is SyncMemoryV1 && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.deletedAt == deletedAt && other.hideAt == hideAt && other.id == id && @@ -99,34 +98,48 @@ class SyncMemoryV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -139,18 +152,18 @@ class SyncMemoryV1 { final json = value.cast(); return SyncMemoryV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + data: mapCastOfType(json, r'data')!, + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart index fc2c36aa8c..1bd6f4a160 100644 --- a/mobile/openapi/lib/model/sync_person_v1.dart +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -88,7 +88,9 @@ class SyncPersonV1 { Map toJson() { final json = {}; if (this.birthDate != null) { - json[r'birthDate'] = this.birthDate!.toUtc().toIso8601String(); + json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.birthDate!.millisecondsSinceEpoch + : this.birthDate!.toUtc().toIso8601String(); } else { // json[r'birthDate'] = null; } @@ -97,7 +99,9 @@ class SyncPersonV1 { } else { // json[r'color'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.faceAssetId != null) { json[r'faceAssetId'] = this.faceAssetId; } else { @@ -108,7 +112,9 @@ class SyncPersonV1 { json[r'isHidden'] = this.isHidden; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -121,16 +127,16 @@ class SyncPersonV1 { final json = value.cast(); return SyncPersonV1( - birthDate: mapDateTime(json, r'birthDate', r''), + birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), color: mapValueOfType(json, r'color'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, faceAssetId: mapValueOfType(json, r'faceAssetId'), id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 671081c0a5..f51cc8bde9 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Sync request types +/// Sync request type class SyncRequestType { /// Instantiate a new enum with the provided [value]. const SyncRequestType._(this.value); diff --git a/mobile/openapi/lib/model/sync_stack_v1.dart b/mobile/openapi/lib/model/sync_stack_v1.dart index e4487ccfaf..3e79a55134 100644 --- a/mobile/openapi/lib/model/sync_stack_v1.dart +++ b/mobile/openapi/lib/model/sync_stack_v1.dart @@ -57,11 +57,15 @@ class SyncStackV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'ownerId'] = this.ownerId; json[r'primaryAssetId'] = this.primaryAssetId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -74,11 +78,11 @@ class SyncStackV1 { final json = value.cast(); return SyncStackV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, ownerId: mapValueOfType(json, r'ownerId')!, primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart index 61340a8f82..67976108e1 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart @@ -17,7 +17,6 @@ class SyncUserMetadataDeleteV1 { required this.userId, }); - /// User metadata key UserMetadataKey key; /// User ID diff --git a/mobile/openapi/lib/model/sync_user_metadata_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_v1.dart index 23803d0be4..ddde7c0513 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_v1.dart @@ -15,23 +15,22 @@ class SyncUserMetadataV1 { SyncUserMetadataV1({ required this.key, required this.userId, - required this.value, + this.value = const {}, }); - /// User metadata key UserMetadataKey key; /// User ID String userId; /// User metadata value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataV1 && other.key == key && other.userId == userId && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +61,7 @@ class SyncUserMetadataV1 { return SyncUserMetadataV1( key: UserMetadataKey.fromJson(json[r'key'])!, userId: mapValueOfType(json, r'userId')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_v1.dart b/mobile/openapi/lib/model/sync_user_v1.dart index 6d425130a3..0a81593547 100644 --- a/mobile/openapi/lib/model/sync_user_v1.dart +++ b/mobile/openapi/lib/model/sync_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncUserV1 { /// Returns a new [SyncUserV1] instance. SyncUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -22,7 +22,6 @@ class SyncUserV1 { required this.profileChangedAt, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -75,7 +74,9 @@ class SyncUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -83,7 +84,9 @@ class SyncUserV1 { json[r'hasProfileImage'] = this.hasProfileImage; json[r'id'] = this.id; json[r'name'] = this.name; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); return json; } @@ -97,12 +100,12 @@ class SyncUserV1 { return SyncUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; @@ -150,7 +153,6 @@ class SyncUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index 6c7acbd218..ecf2e5da4a 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -36,7 +36,6 @@ class SystemConfigFFmpegDto { required this.twoPass, }); - /// Transcode hardware acceleration TranscodeHWAccel accel; /// Accelerated decode @@ -57,7 +56,6 @@ class SystemConfigFFmpegDto { /// Maximum value: 16 int bframes; - /// CQ mode CQMode cqMode; /// CRF @@ -69,6 +67,7 @@ class SystemConfigFFmpegDto { /// GOP size /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int gopSize; /// Max bitrate @@ -86,13 +85,11 @@ class SystemConfigFFmpegDto { /// Maximum value: 6 int refs; - /// Target audio codec AudioCodec targetAudioCodec; /// Target resolution String targetResolution; - /// Target video codec VideoCodec targetVideoCodec; /// Temporal AQ @@ -101,12 +98,11 @@ class SystemConfigFFmpegDto { /// Threads /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int threads; - /// Tone mapping ToneMapping tonemap; - /// Transcode policy TranscodePolicy transcode; /// Two pass diff --git a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart index b5640f82c8..d78f8fadd5 100644 --- a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart @@ -15,18 +15,23 @@ class SystemConfigGeneratedFullsizeImageDto { SystemConfigGeneratedFullsizeImageDto({ required this.enabled, required this.format, - this.progressive = false, + this.progressive, required this.quality, }); /// Enabled bool enabled; - /// Image format ImageFormat format; /// Progressive - bool progressive; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? progressive; /// Quality /// @@ -46,7 +51,7 @@ class SystemConfigGeneratedFullsizeImageDto { // ignore: unnecessary_parenthesis (enabled.hashCode) + (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode); @override @@ -56,7 +61,11 @@ class SystemConfigGeneratedFullsizeImageDto { final json = {}; json[r'enabled'] = this.enabled; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; return json; } @@ -72,7 +81,7 @@ class SystemConfigGeneratedFullsizeImageDto { return SystemConfigGeneratedFullsizeImageDto( enabled: mapValueOfType(json, r'enabled')!, format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, ); } diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart index 3e8fed2c68..2571c0cab0 100644 --- a/mobile/openapi/lib/model/system_config_generated_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart @@ -14,15 +14,21 @@ class SystemConfigGeneratedImageDto { /// Returns a new [SystemConfigGeneratedImageDto] instance. SystemConfigGeneratedImageDto({ required this.format, - this.progressive = false, + this.progressive, required this.quality, required this.size, }); - /// Image format ImageFormat format; - bool progressive; + /// Progressive + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? progressive; /// Quality /// @@ -33,6 +39,7 @@ class SystemConfigGeneratedImageDto { /// Size /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int size; @override @@ -46,7 +53,7 @@ class SystemConfigGeneratedImageDto { int get hashCode => // ignore: unnecessary_parenthesis (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode) + (size.hashCode); @@ -56,7 +63,11 @@ class SystemConfigGeneratedImageDto { Map toJson() { final json = {}; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; json[r'size'] = this.size; return json; @@ -72,7 +83,7 @@ class SystemConfigGeneratedImageDto { return SystemConfigGeneratedImageDto( format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, size: mapValueOfType(json, r'size')!, ); diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 217a666a67..668b740872 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -20,7 +20,6 @@ class SystemConfigImageDto { required this.thumbnail, }); - /// Colorspace Colorspace colorspace; /// Extract embedded diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart index 28ea603c2a..003000d2ec 100644 --- a/mobile/openapi/lib/model/system_config_library_scan_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_scan_dto.dart @@ -17,6 +17,7 @@ class SystemConfigLibraryScanDto { required this.enabled, }); + /// Cron expression String cronExpression; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index 2a0f1ffbc6..6162e72b8f 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -35,6 +35,7 @@ class SystemConfigMachineLearningDto { OcrConfig ocr; + /// ML service URLs List urls; @override diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index 109babd374..7a2fbb516b 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -18,11 +18,13 @@ class SystemConfigMapDto { required this.lightStyle, }); + /// Dark map style URL String darkStyle; /// Enabled bool enabled; + /// Light map style URL String lightStyle; @override diff --git a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart index cfb18b181e..0db417427f 100644 --- a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart +++ b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart @@ -33,6 +33,7 @@ class SystemConfigNightlyTasksDto { /// Missing thumbnails bool missingThumbnails; + /// Start time String startTime; /// Sync quota usage diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index 82195e498b..cc8c38e503 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class SystemConfigOAuthDto { /// Returns a new [SystemConfigOAuthDto] instance. SystemConfigOAuthDto({ + required this.allowInsecureRequests, required this.autoLaunch, required this.autoRegister, required this.buttonText, @@ -33,6 +34,9 @@ class SystemConfigOAuthDto { required this.tokenEndpointAuthMethod, }); + /// Allow insecure requests + bool allowInsecureRequests; + /// Auto launch bool autoLaunch; @@ -51,7 +55,7 @@ class SystemConfigOAuthDto { /// Default storage quota /// /// Minimum value: 0 - int? defaultStorageQuota; + num? defaultStorageQuota; /// Enabled bool enabled; @@ -62,7 +66,7 @@ class SystemConfigOAuthDto { /// Mobile override enabled bool mobileOverrideEnabled; - /// Mobile redirect URI + /// Mobile redirect URI (set to empty string to disable) String mobileRedirectUri; /// Profile signing algorithm @@ -74,6 +78,7 @@ class SystemConfigOAuthDto { /// Scope String scope; + /// Signing algorithm String signingAlgorithm; /// Storage label claim @@ -85,13 +90,14 @@ class SystemConfigOAuthDto { /// Timeout /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int timeout; - /// Token endpoint auth method OAuthTokenEndpointAuthMethod tokenEndpointAuthMethod; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto && + other.allowInsecureRequests == allowInsecureRequests && other.autoLaunch == autoLaunch && other.autoRegister == autoRegister && other.buttonText == buttonText && @@ -114,6 +120,7 @@ class SystemConfigOAuthDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (allowInsecureRequests.hashCode) + (autoLaunch.hashCode) + (autoRegister.hashCode) + (buttonText.hashCode) + @@ -134,10 +141,11 @@ class SystemConfigOAuthDto { (tokenEndpointAuthMethod.hashCode); @override - String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; + String toString() => 'SystemConfigOAuthDto[allowInsecureRequests=$allowInsecureRequests, autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; Map toJson() { final json = {}; + json[r'allowInsecureRequests'] = this.allowInsecureRequests; json[r'autoLaunch'] = this.autoLaunch; json[r'autoRegister'] = this.autoRegister; json[r'buttonText'] = this.buttonText; @@ -172,12 +180,15 @@ class SystemConfigOAuthDto { final json = value.cast(); return SystemConfigOAuthDto( + allowInsecureRequests: mapValueOfType(json, r'allowInsecureRequests')!, autoLaunch: mapValueOfType(json, r'autoLaunch')!, autoRegister: mapValueOfType(json, r'autoRegister')!, buttonText: mapValueOfType(json, r'buttonText')!, clientId: mapValueOfType(json, r'clientId')!, clientSecret: mapValueOfType(json, r'clientSecret')!, - defaultStorageQuota: mapValueOfType(json, r'defaultStorageQuota'), + defaultStorageQuota: json[r'defaultStorageQuota'] == null + ? null + : num.parse('${json[r'defaultStorageQuota']}'), enabled: mapValueOfType(json, r'enabled')!, issuerUrl: mapValueOfType(json, r'issuerUrl')!, mobileOverrideEnabled: mapValueOfType(json, r'mobileOverrideEnabled')!, @@ -237,6 +248,7 @@ class SystemConfigOAuthDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'allowInsecureRequests', 'autoLaunch', 'autoRegister', 'buttonText', diff --git a/mobile/openapi/lib/model/system_config_template_emails_dto.dart b/mobile/openapi/lib/model/system_config_template_emails_dto.dart index 9db85509f5..d29ca1fac3 100644 --- a/mobile/openapi/lib/model/system_config_template_emails_dto.dart +++ b/mobile/openapi/lib/model/system_config_template_emails_dto.dart @@ -18,10 +18,13 @@ class SystemConfigTemplateEmailsDto { required this.welcomeTemplate, }); + /// Album invite template String albumInviteTemplate; + /// Album update template String albumUpdateTemplate; + /// Welcome template String welcomeTemplate; @override diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart index 9bdaef92d3..790710751f 100644 --- a/mobile/openapi/lib/model/system_config_trash_dto.dart +++ b/mobile/openapi/lib/model/system_config_trash_dto.dart @@ -20,6 +20,7 @@ class SystemConfigTrashDto { /// Days /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int days; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart index a7313560e6..dc553e7369 100644 --- a/mobile/openapi/lib/model/system_config_user_dto.dart +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -19,6 +19,7 @@ class SystemConfigUserDto { /// Delete delay /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int deleteDelay; @override diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart index 5566846e3c..4d689f01a1 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -17,6 +17,9 @@ class TagBulkAssetsResponseDto { }); /// Number of assets tagged + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart index fd6a10163c..e05b29f1ed 100644 --- a/mobile/openapi/lib/model/tag_create_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -19,12 +19,6 @@ class TagCreateDto { }); /// Tag color (hex) - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? color; /// Tag name diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart index 1e4a4bd109..8a3ac17474 100644 --- a/mobile/openapi/lib/model/tags_response.dart +++ b/mobile/openapi/lib/model/tags_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class TagsResponse { /// Returns a new [TagsResponse] instance. TagsResponse({ - this.enabled = true, - this.sidebarWeb = true, + required this.enabled, + required this.sidebarWeb, }); /// Whether tags are enabled diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart index 720323cd14..e2f9bec1ec 100644 --- a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart @@ -39,7 +39,7 @@ class TimeBucketAssetResponseDto { /// Array of country names extracted from EXIF GPS data List country; - /// Array of video durations in HH:MM:SS format (null for images) + /// Array of video/gif durations in hh:mm:ss.SSS format (null for static images) List duration; /// Array of file creation timestamps in UTC diff --git a/mobile/openapi/lib/model/time_buckets_response_dto.dart b/mobile/openapi/lib/model/time_buckets_response_dto.dart index 11faa815e2..8b8da1d37a 100644 --- a/mobile/openapi/lib/model/time_buckets_response_dto.dart +++ b/mobile/openapi/lib/model/time_buckets_response_dto.dart @@ -18,6 +18,9 @@ class TimeBucketsResponseDto { }); /// Number of assets in this time bucket + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; /// Time bucket identifier in YYYY-MM-DD format representing the start of the time period diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart index 7edd5d032a..7b43d9ceb7 100644 --- a/mobile/openapi/lib/model/trash_response_dto.dart +++ b/mobile/openapi/lib/model/trash_response_dto.dart @@ -17,6 +17,9 @@ class TrashResponseDto { }); /// Number of items in trash + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index 46ce8b0ecc..ae4a5c1f87 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -56,7 +56,6 @@ class UpdateAlbumDto { /// bool? isActivityEnabled; - /// Asset sort order /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index 9d934eb465..43218cae6e 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -16,7 +16,6 @@ class UpdateAlbumUserDto { required this.role, }); - /// Album user role AlbumUserRole role; @override diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 8526995934..2c4c3352ea 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -52,6 +52,9 @@ class UpdateAssetDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -64,6 +67,9 @@ class UpdateAssetDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -75,9 +81,8 @@ class UpdateAssetDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; - /// Asset visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -172,9 +177,7 @@ class UpdateAssetDto { latitude: num.parse('${json[r'latitude']}'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index 628bdc0055..276d43ecd9 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -13,16 +13,16 @@ part of openapi.api; class UpdateLibraryDto { /// Returns a new [UpdateLibraryDto] instance. UpdateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -51,8 +51,8 @@ class UpdateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -71,11 +71,11 @@ class UpdateLibraryDto { return UpdateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ); } diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index da1fe600a5..462b82c3e0 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -24,18 +24,33 @@ class UsageByUserDto { }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// User quota size in bytes (null if unlimited) + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// User ID @@ -45,6 +60,9 @@ class UsageByUserDto { String userName; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index 485b2e00e5..54da0b0566 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -25,7 +25,6 @@ class UserAdminCreateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -61,6 +60,7 @@ class UserAdminCreateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index 706f65cf35..09f8cedce4 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -32,7 +32,6 @@ class UserAdminResponseDto { required this.updatedAt, }); - /// Avatar color UserAvatarColor avatarColor; /// Creation date @@ -50,7 +49,6 @@ class UserAdminResponseDto { /// Is admin user bool isAdmin; - /// User license UserLicense? license; /// User name @@ -66,15 +64,20 @@ class UserAdminResponseDto { String profileImagePath; /// Storage quota in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Storage usage in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaUsageInBytes; /// Require password change on next login bool shouldChangePassword; - /// User status UserStatus status; /// Storage label @@ -130,9 +133,13 @@ class UserAdminResponseDto { Map toJson() { final json = {}; json[r'avatarColor'] = this.avatarColor; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -165,7 +172,9 @@ class UserAdminResponseDto { } else { // json[r'storageLabel'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -179,8 +188,8 @@ class UserAdminResponseDto { return UserAdminResponseDto( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, - createdAt: mapDateTime(json, r'createdAt', r'')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, @@ -194,7 +203,7 @@ class UserAdminResponseDto { shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, status: UserStatus.fromJson(json[r'status'])!, storageLabel: mapValueOfType(json, r'storageLabel'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index 3cce65745f..0c33a46139 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -24,7 +24,6 @@ class UserAdminUpdateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -69,6 +68,7 @@ class UserAdminUpdateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_avatar_color.dart b/mobile/openapi/lib/model/user_avatar_color.dart index 4fcf518550..719e366899 100644 --- a/mobile/openapi/lib/model/user_avatar_color.dart +++ b/mobile/openapi/lib/model/user_avatar_color.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Avatar color +/// User avatar color class UserAvatarColor { /// Instantiate a new enum with the provided [value]. const UserAvatarColor._(this.value); diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index f02dc73bef..8ef46a0bb5 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -24,7 +24,7 @@ class UserLicense { /// Activation key String activationKey; - /// License key + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override @@ -45,7 +45,9 @@ class UserLicense { Map toJson() { final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); + json[r'activatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.activatedAt.millisecondsSinceEpoch + : this.activatedAt.toUtc().toIso8601String(); json[r'activationKey'] = this.activationKey; json[r'licenseKey'] = this.licenseKey; return json; @@ -60,7 +62,7 @@ class UserLicense { final json = value.cast(); return UserLicense( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, + activatedAt: mapDateTime(json, r'activatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, activationKey: mapValueOfType(json, r'activationKey')!, licenseKey: mapValueOfType(json, r'licenseKey')!, ); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index bf0e2cbf09..f671072c72 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -21,7 +21,6 @@ class UserResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 066c435eb3..0751d4096b 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -19,7 +19,6 @@ class UserUpdateMeDto { this.password, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 59c3680782..68fb0e9fe2 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -13,15 +13,15 @@ part of openapi.api; class ValidateLibraryDto { /// Returns a new [ValidateLibraryDto] instance. ValidateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths to validate (max 128) - Set importPaths; + List importPaths; @override bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto && @@ -39,8 +39,8 @@ class ValidateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; return json; } @@ -54,11 +54,11 @@ class ValidateLibraryDto { return ValidateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart index 78cc03dc94..ebcb881935 100644 --- a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart @@ -14,7 +14,7 @@ class ValidateLibraryImportPathResponseDto { /// Returns a new [ValidateLibraryImportPathResponseDto] instance. ValidateLibraryImportPathResponseDto({ required this.importPath, - this.isValid = false, + required this.isValid, this.message, }); diff --git a/mobile/openapi/lib/model/video_container.dart b/mobile/openapi/lib/model/video_container.dart index b1a47c8721..a291fabf6e 100644 --- a/mobile/openapi/lib/model/video_container.dart +++ b/mobile/openapi/lib/model/video_container.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Accepted containers +/// Accepted video containers class VideoContainer { /// Instantiate a new enum with the provided [value]. const VideoContainer._(this.value); diff --git a/mobile/openapi/lib/model/workflow_action_item_dto.dart b/mobile/openapi/lib/model/workflow_action_item_dto.dart index 9222dd6ba7..1ad70238d8 100644 --- a/mobile/openapi/lib/model/workflow_action_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowActionItemDto { /// Returns a new [WorkflowActionItemDto] instance. WorkflowActionItemDto({ - this.actionConfig, + this.actionConfig = const {}, required this.pluginActionId, }); - /// Action configuration - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? actionConfig; + Map actionConfig; /// Plugin action ID String pluginActionId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionItemDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.pluginActionId == pluginActionId; @override int get hashCode => // ignore: unnecessary_parenthesis - (actionConfig == null ? 0 : actionConfig!.hashCode) + + (actionConfig.hashCode) + (pluginActionId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowActionItemDto { Map toJson() { final json = {}; - if (this.actionConfig != null) { json[r'actionConfig'] = this.actionConfig; - } else { - // json[r'actionConfig'] = null; - } json[r'pluginActionId'] = this.pluginActionId; return json; } @@ -63,7 +52,7 @@ class WorkflowActionItemDto { final json = value.cast(); return WorkflowActionItemDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig') ?? const {}, pluginActionId: mapValueOfType(json, r'pluginActionId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_action_response_dto.dart b/mobile/openapi/lib/model/workflow_action_response_dto.dart index 8f77e9cf2b..dcbb5ee8ef 100644 --- a/mobile/openapi/lib/model/workflow_action_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowActionResponseDto { required this.workflowId, }); - /// Action configuration - Object? actionConfig; + Map? actionConfig; /// Action ID String id; @@ -37,7 +36,7 @@ class WorkflowActionResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionResponseDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.id == id && other.order == order && other.pluginActionId == pluginActionId && @@ -78,7 +77,7 @@ class WorkflowActionResponseDto { final json = value.cast(); return WorkflowActionResponseDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginActionId: mapValueOfType(json, r'pluginActionId')!, diff --git a/mobile/openapi/lib/model/workflow_create_dto.dart b/mobile/openapi/lib/model/workflow_create_dto.dart index 38665a1912..143af0ca6c 100644 --- a/mobile/openapi/lib/model/workflow_create_dto.dart +++ b/mobile/openapi/lib/model/workflow_create_dto.dart @@ -48,7 +48,6 @@ class WorkflowCreateDto { /// Workflow name String name; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_filter_item_dto.dart b/mobile/openapi/lib/model/workflow_filter_item_dto.dart index 52e29c3e93..92224b9f16 100644 --- a/mobile/openapi/lib/model/workflow_filter_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowFilterItemDto { /// Returns a new [WorkflowFilterItemDto] instance. WorkflowFilterItemDto({ - this.filterConfig, + this.filterConfig = const {}, required this.pluginFilterId, }); - /// Filter configuration - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? filterConfig; + Map filterConfig; /// Plugin filter ID String pluginFilterId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterItemDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.pluginFilterId == pluginFilterId; @override int get hashCode => // ignore: unnecessary_parenthesis - (filterConfig == null ? 0 : filterConfig!.hashCode) + + (filterConfig.hashCode) + (pluginFilterId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowFilterItemDto { Map toJson() { final json = {}; - if (this.filterConfig != null) { json[r'filterConfig'] = this.filterConfig; - } else { - // json[r'filterConfig'] = null; - } json[r'pluginFilterId'] = this.pluginFilterId; return json; } @@ -63,7 +52,7 @@ class WorkflowFilterItemDto { final json = value.cast(); return WorkflowFilterItemDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig') ?? const {}, pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_filter_response_dto.dart b/mobile/openapi/lib/model/workflow_filter_response_dto.dart index 355378adac..932722f5a5 100644 --- a/mobile/openapi/lib/model/workflow_filter_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowFilterResponseDto { required this.workflowId, }); - /// Filter configuration - Object? filterConfig; + Map? filterConfig; /// Filter ID String id; @@ -37,7 +36,7 @@ class WorkflowFilterResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterResponseDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.id == id && other.order == order && other.pluginFilterId == pluginFilterId && @@ -78,7 +77,7 @@ class WorkflowFilterResponseDto { final json = value.cast(); return WorkflowFilterResponseDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, diff --git a/mobile/openapi/lib/model/workflow_response_dto.dart b/mobile/openapi/lib/model/workflow_response_dto.dart index ae3e6510aa..6461b62508 100644 --- a/mobile/openapi/lib/model/workflow_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_response_dto.dart @@ -48,7 +48,6 @@ class WorkflowResponseDto { /// Owner user ID String ownerId; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_update_dto.dart b/mobile/openapi/lib/model/workflow_update_dto.dart index 9891fff079..9abb45ddd5 100644 --- a/mobile/openapi/lib/model/workflow_update_dto.dart +++ b/mobile/openapi/lib/model/workflow_update_dto.dart @@ -54,7 +54,6 @@ class WorkflowUpdateDto { /// String? name; - /// Workflow trigger type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/packages/ui/pubspec.lock b/mobile/packages/ui/pubspec.lock index 697e1debf5..4ac863d0f7 100644 --- a/mobile/packages/ui/pubspec.lock +++ b/mobile/packages/ui/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -87,26 +87,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -164,10 +164,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.10" vector_math: dependency: transitive description: @@ -185,5 +185,5 @@ packages: source: hosted version: "15.0.2" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/mobile/packages/ui/pubspec.yaml b/mobile/packages/ui/pubspec.yaml index a25dfb6ca4..de50e0a429 100644 --- a/mobile/packages/ui/pubspec.yaml +++ b/mobile/packages/ui/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_ui publish_to: none environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.11.0 <4.0.0' dependencies: flutter: diff --git a/mobile/packages/ui/showcase/pubspec.lock b/mobile/packages/ui/showcase/pubspec.lock index c79e6c18c7..c676b23c53 100644 --- a/mobile/packages/ui/showcase/pubspec.lock +++ b/mobile/packages/ui/showcase/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -124,10 +124,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a + sha256: "5540e4a3f416dd4a93458257b908eb88353cbd0fb5b0a3d1bd7d849ba1e88735" url: "https://pub.dev" source: hosted - version: "17.0.1" + version: "17.2.1" immich_ui: dependency: "direct main" description: @@ -195,26 +195,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -312,10 +312,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.10" typed_data: dependency: transitive description: @@ -373,5 +373,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.9.2 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.35.0" diff --git a/mobile/packages/ui/showcase/pubspec.yaml b/mobile/packages/ui/showcase/pubspec.yaml index e45ce07e66..6353600ce3 100644 --- a/mobile/packages/ui/showcase/pubspec.yaml +++ b/mobile/packages/ui/showcase/pubspec.yaml @@ -4,14 +4,14 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ^3.9.2 + sdk: ^3.11.0 dependencies: flutter: sdk: flutter immich_ui: path: ../ - go_router: ^17.0.1 + go_router: ^17.2.1 syntax_highlight: ^0.5.0 dev_dependencies: diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9d5f431792..e53b500cf8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -378,14 +378,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" dbus: dependency: transitive description: @@ -977,13 +969,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" - immich_mobile_immich_lint: - dependency: "direct dev" - description: - path: immich_lint - relative: true - source: path - version: "0.0.0" immich_ui: dependency: "direct main" description: @@ -1012,40 +997,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - isar: - dependency: "direct main" - description: - path: "packages/isar" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" - isar_community: - dependency: transitive - description: - name: isar_community - sha256: "28f59e54636c45ba0bb1b3b7f2656f1c50325f740cea6efcd101900be3fba546" - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_community_flutter_libs: - dependency: "direct main" - description: - name: isar_community_flutter_libs - sha256: c2934fe755bb3181cb67602fd5df0d080b3d3eb52799f98623aa4fc5acbea010 - url: "https://pub.dev" - source: hosted - version: "3.3.0-dev.3" - isar_generator: - dependency: "direct dev" - description: - path: "packages/isar_generator" - ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - resolved-ref: bb1dca40fe87a001122e5d43bc6254718cb49f3a - url: "https://github.com/immich-app/isar" - source: git - version: "3.1.8" jni: dependency: transitive description: @@ -1178,26 +1129,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1897,10 +1848,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.10" thumbhash: dependency: "direct main" description: @@ -1909,14 +1860,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0+1" - time: - dependency: transitive - description: - name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" - url: "https://pub.dev" - source: hosted - version: "2.1.5" timezone: dependency: "direct main" description: @@ -2173,14 +2116,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" - url: "https://pub.dev" - source: hosted - version: "1.2.0" yaml: dependency: transitive description: @@ -2190,5 +2125,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.7" + dart: ">=3.11.0 <4.0.0" + flutter: "3.41.6" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 7101cd91ad..4b06e08179 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,11 +2,11 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 2.7.4+3045 +version: 2.7.5+3046 environment: - sdk: '>=3.8.0 <4.0.0' - flutter: 3.35.7 + sdk: '>=3.11.0 <4.0.0' + flutter: 3.41.6 dependencies: async: ^2.13.0 @@ -43,16 +43,9 @@ dependencies: immich_ui: path: './packages/ui' intl: ^0.20.2 - isar: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar/ - isar_community_flutter_libs: 3.3.0-dev.3 local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.22.0 - native_video_player: git: url: https://github.com/immich-app/native_video_player @@ -111,15 +104,8 @@ dev_dependencies: flutter_native_splash: ^2.4.7 flutter_test: sdk: flutter - immich_mobile_immich_lint: - path: './immich_lint' integration_test: sdk: flutter - isar_generator: - git: - url: https://github.com/immich-app/isar - ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a' - path: packages/isar_generator/ mocktail: ^1.0.4 # Type safe platform code pigeon: ^26.0.2 diff --git a/mobile/scripts/fdroid_build_isar.sh b/mobile/scripts/fdroid_build_isar.sh deleted file mode 100755 index a145268356..0000000000 --- a/mobile/scripts/fdroid_build_isar.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env sh - -test -d .isar || exit -cp .isar-cargo.lock .isar/Cargo.lock -(cd .isar || exit -bash tool/build_android.sh x86 -bash tool/build_android.sh x64 -bash tool/build_android.sh armv7 -bash tool/build_android.sh arm64 -mv libisar_android_arm64.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/arm64-v8a/ -mv libisar_android_armv7.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/armeabi-v7a/ -mv libisar_android_x64.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/x86_64/ -mv libisar_android_x86.so libisar.so -mv libisar.so ../.pub-cache/hosted/pub.dev/isar_community_flutter_libs-*/android/src/main/jniLibs/x86/ -) diff --git a/mobile/scripts/fdroid_update_isar.sh b/mobile/scripts/fdroid_update_isar.sh deleted file mode 100755 index 814f50a8a1..0000000000 --- a/mobile/scripts/fdroid_update_isar.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env sh - -isar_version="$(awk '/isar: /{gsub(/\^/, "", $2); print $2}' pubspec.yaml)" -checked_out_version="$(git -C .isar describe --tags)" - -if [ "$isar_version" = "$checked_out_version" ]; then - echo "isar is up-to-date." - exit 0 -fi -echo "Updating from version $checked_out_version to $isar_version." - -git -C .isar checkout "$isar_version" -cargo generate-lockfile --manifest-path .isar/Cargo.toml -mv .isar/Cargo.lock .isar-cargo.lock diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart index c6a3a90582..e1c32eaaee 100644 --- a/mobile/test/api.mocks.dart +++ b/mobile/test/api.mocks.dart @@ -1,8 +1,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; -class MockAssetsApi extends Mock implements AssetsApi {} - class MockSyncApi extends Mock implements SyncApi {} class MockServerApi extends Mock implements ServerApi {} diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 56b4802f88..89e85a3794 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,20 +1,13 @@ import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} -class MockUserService extends Mock implements UserService {} - class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockAppSettingsService extends Mock implements AppSettingsService {} - -class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} - diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart deleted file mode 100644 index 9110a09471..0000000000 --- a/mobile/test/domain/services/album.service_test.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late RemoteAlbumService sut; - late DriftRemoteAlbumRepository mockRemoteAlbumRepo; - late DriftAlbumApiRepository mockAlbumApiRepo; - - final albumA = RemoteAlbum( - id: '1', - name: 'Album A', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.asc, - assetCount: 1, - createdAt: DateTime(2023, 1, 1), - updatedAt: DateTime(2023, 1, 2), - ownerId: 'owner1', - ownerName: "Test User", - isShared: false, - ); - - final albumB = RemoteAlbum( - id: '2', - name: 'Album B', - description: "", - isActivityEnabled: false, - order: AlbumAssetOrder.desc, - assetCount: 2, - createdAt: DateTime(2023, 2, 1), - updatedAt: DateTime(2023, 2, 2), - ownerId: 'owner2', - ownerName: "Test User", - isShared: false, - ); - - setUp(() { - mockRemoteAlbumRepo = MockRemoteAlbumRepository(); - mockAlbumApiRepo = MockDriftAlbumApiRepository(); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end), - ).thenAnswer((_) async => ['1', '2']); - - when( - () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start), - ).thenAnswer((_) async => ['1', '2']); - - sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); - }); - - group('sortAlbums', () { - test('should sort correctly based on name', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.title); - expect(result, [albumA, albumB]); - }); - - test('should sort correctly based on createdAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.created); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on updatedAt', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on assetCount', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on newestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); - expect(result, [albumB, albumA]); - }); - - test('should sort correctly based on oldestAssetTimestamp', () async { - final albums = [albumB, albumA]; - - final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); - expect(result, [albumA, albumB]); - }); - - test('should flip order when isReverse is true for all modes', () async { - final albums = [albumB, albumA]; - - for (final mode in AlbumSortMode.values) { - final normal = await sut.sortAlbums(albums, mode, isReverse: false); - final reversed = await sut.sortAlbums(albums, mode, isReverse: true); - - // reversed should be the exact inverse of normal - expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode'); - } - }); - }); -} diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart deleted file mode 100644 index 04e49f89f9..0000000000 --- a/mobile/test/domain/services/asset.service_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; -import '../../test_utils.dart'; - -void main() { - late AssetService sut; - late MockRemoteAssetRepository mockRemoteAssetRepository; - late MockDriftLocalAssetRepository mockLocalAssetRepository; - - setUp(() { - mockRemoteAssetRepository = MockRemoteAssetRepository(); - mockLocalAssetRepository = MockDriftLocalAssetRepository(); - sut = AssetService( - remoteAssetRepository: mockRemoteAssetRepository, - localAssetRepository: mockLocalAssetRepository, - ); - }); - - group('getAspectRatio', () { - test('flips dimensions on Android for 90° and 270° orientations', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android'); - } - }); - - test('does not flip dimensions on iOS regardless of orientation', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - for (final orientation in [0, 90, 270]) { - final localAsset = TestUtils.createLocalAsset( - id: 'local-$orientation', - width: 1920, - height: 1080, - orientation: orientation, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions'); - } - }); - - test('fetches dimensions from remote repository when missing from asset', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080); - verify(() => mockRemoteAssetRepository.get('remote-1')).called(1); - }); - - test('fetches dimensions from local repository when missing from local asset', () async { - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('uses fetched asset orientation when dimensions are missing on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - // Original asset has default orientation 0, but dimensions are missing - final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); - - // Fetched asset has 90° orientation and proper dimensions - final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90); - - when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); - - final result = await sut.getAspectRatio(localAsset); - - // Should flip dimensions since fetched asset has 90° orientation - expect(result, 1080 / 1920); - verify(() => mockLocalAssetRepository.get('local-1')).called(1); - }); - - test('returns 1.0 when dimensions are still unavailable after fetching', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('returns 1.0 when height is zero', () async { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0); - - final exif = const ExifInfo(orientation: '1'); - - when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1.0); - }); - - test('handles local asset with remoteId using local orientation not remote exif', () async { - // When a LocalAsset has a remoteId (merged), we should use local orientation - // because the width/height come from the local asset (pre-corrected on iOS) - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 0, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1920 / 1080); - // Should not call remote exif for LocalAsset - verifyNever(() => mockRemoteAssetRepository.getExif(any())); - }); - - test('handles local asset with remoteId and 90 degree rotation on Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - - final localAsset = TestUtils.createLocalAsset( - id: 'local-1', - remoteId: 'remote-1', - width: 1920, - height: 1080, - orientation: 90, - ); - - final result = await sut.getAspectRatio(localAsset); - - expect(result, 1080 / 1920); - }); - - test('should not flip remote asset dimensions', () async { - final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90']; - - for (final orientation in flippedOrientations) { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); - - final exif = ExifInfo(orientation: orientation); - - when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation'); - } - }); - }); -} diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index 95f677ba98..0ccef393ab 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -29,11 +29,11 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; late LogRepository mockLogRepo; - late IsarStoreRepository mockStoreRepo; + late DriftStoreRepository mockStoreRepo; setUp(() async { mockLogRepo = MockLogRepository(); - mockStoreRepo = MockStoreRepository(); + mockStoreRepo = MockDriftStoreRepository(); registerFallbackValue(_kInfoLog); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 996170b518..8ceb1e3c9c 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -15,13 +15,11 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; - late IsarStoreRepository mockStoreRepo; late DriftStoreRepository mockDriftStoreRepo; late StreamController>> controller; setUp(() async { controller = StreamController>>.broadcast(); - mockStoreRepo = MockStoreRepository(); mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); @@ -29,16 +27,6 @@ void main() { registerFallbackValue(StoreKey.backgroundBackup); registerFallbackValue(StoreKey.backupFailedSince); - when(() => mockStoreRepo.getAll()).thenAnswer( - (_) async => [ - const StoreDto(StoreKey.accessToken, _kAccessToken), - const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup), - const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy), - StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince), - ], - ); - when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - when(() => mockDriftStoreRepo.getAll()).thenAnswer( (_) async => [ const StoreDto(StoreKey.accessToken, _kAccessToken), @@ -49,7 +37,7 @@ void main() { ); when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - sut = await StoreService.create(storeRepository: mockStoreRepo); + sut = await StoreService.create(storeRepository: mockDriftStoreRepo); }); tearDown(() async { @@ -59,7 +47,7 @@ void main() { group("Store Service Init:", () { test('Populates the internal cache on init', () { - verify(() => mockStoreRepo.getAll()).called(1); + verify(() => mockDriftStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup); expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy); @@ -74,7 +62,7 @@ void main() { await pumpEventQueue(); - verify(() => mockStoreRepo.watchAll()).called(1); + verify(() => mockDriftStoreRepo.watchAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase()); }); }); @@ -95,19 +83,18 @@ void main() { group('Store Service put:', () { setUp(() { - when(() => mockStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { await sut.put(StoreKey.accessToken, _kAccessToken); - verifyNever(() => mockStoreRepo.upsert(StoreKey.accessToken, any())); + verifyNever(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, any())); }); test('Insert value when modified', () async { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); - verify(() => mockStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); + verify(() => mockDriftStoreRepo.upsert(StoreKey.accessToken, newAccessToken)).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); }); @@ -117,7 +104,6 @@ void main() { setUp(() { valueController = StreamController.broadcast(); - when(() => mockStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); }); @@ -136,19 +122,18 @@ void main() { } await pumpEventQueue(); - verify(() => mockStoreRepo.watch(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.watch(StoreKey.accessToken)).called(1); }); }); group('Store Service delete:', () { setUp(() { - when(() => mockStoreRepo.delete(any>())).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.delete(any>())).thenAnswer((_) async => true); }); test('Removes the value from the DB', () async { await sut.delete(StoreKey.accessToken); - verify(() => mockStoreRepo.delete(StoreKey.accessToken)).called(1); + verify(() => mockDriftStoreRepo.delete(StoreKey.accessToken)).called(1); }); test('Removes the value from the cache', () async { @@ -159,13 +144,12 @@ void main() { group('Store Service clear:', () { setUp(() { - when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true); when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); }); test('Clears all values from the store', () async { await sut.clear(); - verify(() => mockStoreRepo.deleteAll()).called(1); + verify(() => mockDriftStoreRepo.deleteAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.backgroundBackup), isNull); expect(sut.tryGet(StoreKey.groupAssetsBy), isNull); diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index 395f38a207..80b6d80457 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,19 +13,13 @@ import '../service.mock.dart'; void main() { late UserService sut; - late IsarUserRepository mockUserRepo; late UserApiRepository mockUserApiRepo; late StoreService mockStoreService; setUp(() { - mockUserRepo = MockIsarUserRepository(); mockUserApiRepo = MockUserApiRepository(); mockStoreService = MockStoreService(); - sut = UserService( - isarUserRepository: mockUserRepo, - userApiRepository: mockUserApiRepo, - storeService: mockStoreService, - ); + sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService); registerFallbackValue(UserStub.admin); when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin); @@ -77,11 +70,9 @@ void main() { test('should return user from api and store it', () async { when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin); when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(UserStub.admin)).thenAnswer((_) async => UserStub.admin); final result = await sut.refreshMyUser(); verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1); - verify(() => mockUserRepo.update(UserStub.admin)).called(1); expect(result, UserStub.admin); }); @@ -90,7 +81,6 @@ void main() { final result = await sut.refreshMyUser(); verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)); - verifyNever(() => mockUserRepo.update(UserStub.admin)); expect(result, isNull); }); }); @@ -104,12 +94,10 @@ void main() { () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), ).thenAnswer((_) async => profileImagePath); when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true); - when(() => mockUserRepo.update(updatedUser)).thenAnswer((_) async => UserStub.admin); final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1); - verify(() => mockUserRepo.update(updatedUser)).called(1); expect(result, profileImagePath); }); @@ -123,7 +111,6 @@ void main() { final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser)); - verifyNever(() => mockUserRepo.update(updatedUser)); expect(result, isNull); }); }); diff --git a/mobile/test/dto.mocks.dart b/mobile/test/dto.mocks.dart deleted file mode 100644 index ed53fcdc90..0000000000 --- a/mobile/test/dto.mocks.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -class MockSmartSearchDto extends Mock implements SmartSearchDto {} - -class MockMetadataSearchDto extends Mock implements MetadataSearchDto {} diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index a22a4b72ab..5141540a25 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,108 +1,4 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; - -import 'asset.stub.dart'; -import 'user.stub.dart'; - -final class AlbumStub { - const AlbumStub._(); - - static final emptyAlbum = Album( - name: "empty-album", - localId: "empty-album-local", - remoteId: "empty-album-remote", - createdAt: DateTime(2000), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - ); - - static final sharedWithUser = Album( - name: "empty-album-shared-with-user", - localId: "empty-album-shared-with-user-local", - remoteId: "empty-album-shared-with-user-remote", - createdAt: DateTime(2023), - modifiedAt: DateTime(2023), - shared: true, - activityEnabled: false, - endDate: DateTime(2020), - )..sharedUsers.addAll([User.fromDto(UserStub.admin)]); - - static final oneAsset = Album( - name: "album-with-single-asset", - localId: "album-with-single-asset-local", - remoteId: "album-with-single-asset-remote", - createdAt: DateTime(2022), - modifiedAt: DateTime(2023), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2023), - )..assets.addAll([AssetStub.image1]); - - static final twoAsset = - Album( - name: "album-with-two-assets", - localId: "album-with-two-assets-local", - remoteId: "album-with-two-assets-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..assets.addAll([AssetStub.image1, AssetStub.image2]) - ..activityEnabled = true - ..owner.value = User.fromDto(UserStub.admin); - - static final create2020end2020Album = Album( - name: "create2020update2020Album", - localId: "create2020update2020Album-local", - remoteId: "create2020update2020Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2020), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2020), - ); - static final create2020end2022Album = Album( - name: "create2020update2021Album", - localId: "create2020update2021Album-local", - remoteId: "create2020update2021Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2022), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2022), - ); - static final create2020end2024Album = Album( - name: "create2020update2022Album", - localId: "create2020update2022Album-local", - remoteId: "create2020update2022Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2024), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2024), - ); - static final create2020end2026Album = Album( - name: "create2020update2023Album", - localId: "create2020update2023Album-local", - remoteId: "create2020update2023Album-remote", - createdAt: DateTime(2020), - modifiedAt: DateTime(2026), - shared: false, - activityEnabled: false, - startDate: DateTime(2020), - endDate: DateTime(2026), - ); -} abstract final class LocalAlbumStub { const LocalAlbumStub._(); diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 90a7f11737..473b900271 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -1,59 +1,4 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart' as old; - -final class AssetStub { - const AssetStub._(); - - static final image1 = old.Asset( - checksum: "image1-checksum", - localId: "image1", - remoteId: 'image1-remote', - ownerId: 1, - fileCreatedAt: DateTime(2019), - fileModifiedAt: DateTime(2020), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: old.AssetType.image, - fileName: "image1.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: false), - ); - - static final image2 = old.Asset( - checksum: "image2-checksum", - localId: "image2", - remoteId: 'image2-remote', - ownerId: 1, - fileCreatedAt: DateTime(2000), - fileModifiedAt: DateTime(2010), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.video, - fileName: "image2.jpg", - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: const ExifInfo(isFlipped: true), - ); - - static final image3 = old.Asset( - checksum: "image3-checksum", - localId: "image3", - ownerId: 1, - fileCreatedAt: DateTime(2025), - fileModifiedAt: DateTime(2025), - updatedAt: DateTime.now(), - durationInSeconds: 60, - type: old.AssetType.image, - fileName: "image3.jpg", - isFavorite: true, - isArchived: false, - isTrashed: false, - ); -} abstract final class LocalAssetStub { const LocalAssetStub._(); diff --git a/mobile/test/fixtures/exif.stub.dart b/mobile/test/fixtures/exif.stub.dart deleted file mode 100644 index 5ad9a41761..0000000000 --- a/mobile/test/fixtures/exif.stub.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:immich_mobile/domain/models/exif.model.dart'; - -abstract final class ExifStub { - static final size = const ExifInfo(assetId: 1, fileSize: 1000); - - static final gps = const ExifInfo( - assetId: 2, - latitude: 20, - longitude: 20, - city: 'city', - state: 'state', - country: 'country', - ); - - static final rotated90CW = const ExifInfo(assetId: 3, orientation: "90"); - - static final rotated270CW = const ExifInfo(assetId: 4, orientation: "-90"); -} diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 2ba7177f89..b92ba71e5b 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -12,24 +12,4 @@ abstract final class UserStub { profileChangedAt: DateTime(2021), avatarColor: AvatarColor.green, ); - - static final user1 = UserDto( - id: "user1", - email: "user1@test.com", - name: "user1", - isAdmin: false, - updatedAt: DateTime(2022), - profileChangedAt: DateTime(2022), - avatarColor: AvatarColor.red, - ); - - static final user2 = UserDto( - id: "user2", - email: "user2@test.com", - name: "user2", - isAdmin: false, - updatedAt: DateTime(2023), - profileChangedAt: DateTime(2023), - avatarColor: AvatarColor.primary, - ); } diff --git a/mobile/test/infrastructure/repositories/exif_repository_test.dart b/mobile/test/infrastructure/repositories/exif_repository_test.dart deleted file mode 100644 index 4e7ee4d79d..0000000000 --- a/mobile/test/infrastructure/repositories/exif_repository_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/exif.stub.dart'; -import '../../test_utils.dart'; - -Future _populateExifTable(Isar db) async { - await db.writeTxn(() async { - await db.exifInfos.putAll([ - ExifInfo.fromDto(ExifStub.size), - ExifInfo.fromDto(ExifStub.gps), - ExifInfo.fromDto(ExifStub.rotated90CW), - ExifInfo.fromDto(ExifStub.rotated270CW), - ]); - }); -} - -void main() { - late Isar db; - late IsarExifRepository sut; - - setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarExifRepository(db); - }); - - group("Return with proper orientation", () { - setUp(() async { - await _populateExifTable(db); - }); - - test("isFlipped true for 90CW", () async { - final exif = await sut.get(ExifStub.rotated90CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped true for 270CW", () async { - final exif = await sut.get(ExifStub.rotated270CW.assetId!); - expect(exif!.isFlipped, true); - }); - - test("isFlipped false for the original non-rotated image", () async { - final exif = await sut.get(ExifStub.size.assetId!); - expect(exif!.isFlipped, false); - }); - }); -} diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 18d41e32e0..4cf1adc6b1 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,14 +1,15 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; const _kTestAccessToken = "#TestToken"; final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); @@ -16,30 +17,54 @@ const _kTestVersion = 10; const _kTestColorfulInterface = false; final _kTestUser = UserStub.admin; -Future _addIntStoreValue(Isar db, StoreKey key, int? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: value, strValue: null)); -} - -Future _addStrStoreValue(Isar db, StoreKey key, String? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: null, strValue: value)); -} - -Future _populateStore(Isar db) async { - await db.writeTxn(() async { - await _addIntStoreValue(db, StoreKey.colorfulInterface, _kTestColorfulInterface ? 1 : 0); - await _addIntStoreValue(db, StoreKey.backupFailedSince, _kTestBackupFailed.millisecondsSinceEpoch); - await _addStrStoreValue(db, StoreKey.accessToken, _kTestAccessToken); - await _addIntStoreValue(db, StoreKey.version, _kTestVersion); +Future _populateStore(Drift db) async { + await db.batch((batch) async { + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.colorfulInterface.id), + intValue: const Value(_kTestColorfulInterface ? 1 : 0), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.backupFailedSince.id), + intValue: Value(_kTestBackupFailed.millisecondsSinceEpoch), + stringValue: const Value(null), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.accessToken.id), + intValue: const Value(null), + stringValue: const Value(_kTestAccessToken), + ), + ); + batch.insert( + db.storeEntity, + StoreEntityCompanion( + id: Value(StoreKey.version.id), + intValue: const Value(_kTestVersion), + stringValue: const Value(null), + ), + ); }); } void main() { - late Isar db; - late IsarStoreRepository sut; + late Drift db; + late DriftStoreRepository sut; setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarStoreRepository(db); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + sut = DriftStoreRepository(db); + }); + + tearDown(() async { + await db.close(); }); group('Store Repository converters:', () { @@ -98,10 +123,10 @@ void main() { }); test('deleteAll()', () async { - final count = await db.storeValues.count(); + final count = await db.storeEntity.count().getSingle(); expect(count, isNot(isZero)); await sut.deleteAll(); - unawaited(expectLater(await db.storeValues.count(), isZero)); + unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero)); }); }); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 85eebacb14..d538b567bd 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/utils/semver.dart'; @@ -13,7 +16,6 @@ import 'package:openapi/api.dart'; import '../../api.mocks.dart'; import '../../service.mocks.dart'; -import '../../test_utils.dart'; class MockHttpClient extends Mock implements http.Client {} @@ -38,7 +40,8 @@ void main() { late int testBatchSize = 3; setUpAll(() async { - await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar())); + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); }); setUp(() { @@ -137,7 +140,7 @@ void main() { bool abortWasCalledInCallback = false; final Completer firstBatchReceived = Completer(); - Future onDataCallback(List events, Function() abort, Function() _) async { + Future onDataCallback(List _, Function() abort, Function() _) async { onDataCallCount++; if (onDataCallCount == 1) { abort(); @@ -241,7 +244,7 @@ void main() { final streamError = Exception("Network Error"); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } @@ -267,7 +270,7 @@ void main() { when(() => mockStreamedResponse.stream).thenAnswer((_) => http.ByteStream(errorBodyController.stream)); int onDataCallCount = 0; - Future onDataCallback(List events, Function() _, Function() __) async { + Future onDataCallback(List _, Function() _, Function() __) async { onDataCallCount++; } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 2d4af5b308..b7992c1822 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; @@ -11,22 +10,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockStoreRepository extends Mock implements IsarStoreRepository {} - class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} class MockLogRepository extends Mock implements LogRepository {} -class MockIsarUserRepository extends Mock implements IsarUserRepository {} - -class MockDeviceAssetRepository extends Mock implements IsarDeviceAssetRepository {} - class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} class MockLocalAlbumRepository extends Mock implements DriftLocalAlbumRepository {} diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart deleted file mode 100644 index 39350530ea..0000000000 --- a/mobile/test/modules/activity/activities_page_test.dart +++ /dev/null @@ -1,175 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.user2), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late MockAlbumActivity activityMock; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockCurrentAssetProvider mockCurrentAssetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() async { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1); - activityMock = MockAlbumActivity(_activities); - overrides = [ - albumActivityProvider(AlbumStub.twoAsset.remoteId!, AssetStub.image1.remoteId!).overrideWith(() => activityMock), - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - currentAssetProvider.overrideWith(() => mockCurrentAssetProvider), - ]; - - await db.writeTxn(() async { - await db.clear(); - // Save all assets - await db.users.put(User.fromDto(UserStub.admin)); - await db.assets.putAll([AssetStub.image1, AssetStub.image2]); - await db.albums.put(AlbumStub.twoAsset); - await AlbumStub.twoAsset.owner.save(); - await AlbumStub.twoAsset.assets.save(); - }); - expect(db.albums.countSync(), 1); - expect(db.assets.countSync(), 2); - expect(db.users.countSync(), 1); - }); - - group("App bar", () { - testWidgets("No title when currentAsset != null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNull); - }); - - testWidgets("Album name as title when currentAsset == null", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - mockCurrentAssetProvider.state = null; - await tester.pumpAndSettle(); - - expect(find.text(AlbumStub.twoAsset.name), findsOneWidget); - final listTile = tester.widget(find.byType(AppBar)); - expect(listTile.title, isNotNull); - }); - }); - - group("Body", () { - testWidgets("Contains a stack with Activity List and Activity Input", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ActivityTextField)), findsOneWidget); - - expect(find.descendant(of: find.byType(Stack), matching: find.byType(ListView)), findsOneWidget); - }); - - testWidgets("List Contains all dismissible activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final listFinder = find.descendant(of: find.byType(Stack), matching: find.byType(ListView)); - final listChildren = find.descendant(of: listFinder, matching: find.byType(DismissibleActivity)); - expect(listChildren, findsNWidgets(_activities.length)); - }); - - testWidgets("Submitting text input adds a comment with the text", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value()); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'Test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - verify(() => activityMock.addComment('Test comment')); - }); - - testWidgets("Owner can remove all activities", (tester) async { - await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides); - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.length)); - }); - - testWidgets("Non-Owner can remove only their activities", (tester) async { - final mockCurrentUser = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - const ActivitiesPage(), - overrides: [...overrides, currentUserProvider.overrideWith((ref) => mockCurrentUser)], - ); - mockCurrentUser.state = UserStub.user1; - await tester.pumpAndSettle(); - - final deletableActivityFinder = find.byWidgetPredicate( - (widget) => widget is DismissibleActivity && widget.onDismiss != null, - ); - expect(deletableActivityFinder, findsNWidgets(_activities.where((a) => a.user == UserStub.user1).length)); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_mocks.dart b/mobile/test/modules/activity/activity_mocks.dart deleted file mode 100644 index c50810795e..0000000000 --- a/mobile/test/modules/activity/activity_mocks.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:immich_mobile/services/activity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class ActivityServiceMock extends Mock implements ActivityService {} - -class MockAlbumActivity extends AlbumActivityInternal with Mock implements AlbumActivity { - List? initActivities; - MockAlbumActivity([this.initActivities]); - - @override - Future> build(String albumId, [String? assetId]) async { - return initActivities ?? []; - } -} - -class ActivityStatisticsMock extends ActivityStatisticsInternal with Mock implements ActivityStatistics {} diff --git a/mobile/test/modules/activity/activity_provider_test.dart b/mobile/test/modules/activity/activity_provider_test.dart deleted file mode 100644 index 84eba62b70..0000000000 --- a/mobile/test/modules/activity/activity_provider_test.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -final _activities = [ - Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'First Activity', - assetId: 'asset-2', - user: UserStub.admin, - ), - Activity( - id: '2', - createdAt: DateTime(200), - type: ActivityType.comment, - comment: 'Second Activity', - user: UserStub.user1, - ), - Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.admin), - Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1), -]; - -void main() { - late ActivityServiceMock activityMock; - late ActivityStatisticsMock activityStatisticsMock; - late ActivityStatisticsMock albumActivityStatisticsMock; - late ProviderContainer container; - late AlbumActivityProvider provider; - late ListenerMock>> listener; - - setUpAll(() { - registerFallbackValue(AsyncData>([..._activities])); - }); - - setUp(() async { - activityMock = ActivityServiceMock(); - activityStatisticsMock = ActivityStatisticsMock(); - albumActivityStatisticsMock = ActivityStatisticsMock(); - - container = TestUtils.createContainer( - overrides: [ - activityServiceProvider.overrideWith((ref) => activityMock), - activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock), - activityStatisticsProvider('test-album').overrideWith(() => albumActivityStatisticsMock), - ], - ); - - // Mock values - when(() => activityStatisticsMock.build(any(), any())).thenReturn(0); - when(() => albumActivityStatisticsMock.build(any())).thenReturn(0); - when( - () => activityMock.getAllActivities('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => [..._activities]); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - // Init and wait for providers future to complete - provider = albumActivityProvider('test-album', 'test-asset'); - listener = ListenerMock(); - container.listen(provider, listener.call, fireImmediately: true); - - await container.read(provider.future); - }); - - test('Returns a list of activity', () async { - verifyInOrder([ - () => listener.call(null, const AsyncLoading()), - () => listener.call( - const AsyncLoading(), - any( - that: allOf([ - isA>>(), - predicate((AsyncData> ad) => ad.requireValue.every((e) => _activities.contains(e))), - ]), - ), - ), - ]); - - verifyNoMoreInteractions(listener); - }); - - group('addLike()', () { - test('Like successfully added', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncData(like)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(like)); - - // Never bump activity count for new likes - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(like)); - }); - - test('Like failed', () async { - final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin); - when( - () => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'), - ).thenAnswer((_) async => AsyncError(Exception('Mock'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addLike(); - - verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(like))); - - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(like))); - }); - }); - - group('removeActivity()', () { - test('Like successfully removed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('3'); - - verify(() => activityMock.removeActivity('3')); - - final activities = await container.read(provider.future); - expect(activities, hasLength(3)); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Remove Like failed', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => false); - - await container.read(provider.notifier).removeActivity('3'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, anyElement(predicate((Activity a) => a.id == '3'))); - - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Comment successfully removed', () async { - when(() => activityMock.removeActivity('1')).thenAnswer((_) async => true); - - await container.read(provider.notifier).removeActivity('1'); - - final activities = await container.read(provider.future); - expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '1')))); - - verify(() => activityStatisticsMock.removeActivity()); - verify(() => albumActivityStatisticsMock.removeActivity()); - }); - - test('Removes activity from album state when asset scoped', () async { - when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).removeActivity('3'); - - final assetActivities = container.read(provider).requireValue; - final albumActivities = container.read(albumProvider).requireValue; - - expect(assetActivities, hasLength(3)); - expect(assetActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - expect(albumActivities, hasLength(3)); - expect(albumActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - - verify(() => activityMock.removeActivity('3')); - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); - }); - }); - - group('addComment()', () { - test('Comment successfully added', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => activityStatisticsMock.build('test-album', 'test-asset')).thenReturn(4); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - - await container.read(provider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ); - - final activities = await container.read(provider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verify(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(5)); - expect(albumActivities, contains(comment)); - }); - - test('Comment successfully added without assetId', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - assetId: 'test-asset', - comment: 'Test-Comment', - ); - - when( - () => activityMock.addActivity('test-album', ActivityType.comment, comment: 'Test-Comment'), - ).thenAnswer((_) async => AsyncData(comment)); - when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); - when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - await container.read(albumProvider.notifier).addComment('Test-Comment'); - - verify( - () => activityMock.addActivity('test-album', ActivityType.comment, assetId: null, comment: 'Test-Comment'), - ); - - final activities = await container.read(albumProvider.future); - expect(activities, hasLength(5)); - expect(activities, contains(comment)); - - verifyNever(() => activityStatisticsMock.addActivity()); - verify(() => albumActivityStatisticsMock.addActivity()); - }); - - test('Comment failed', () async { - final comment = Activity( - id: '5', - createdAt: DateTime(2023), - type: ActivityType.comment, - user: UserStub.admin, - comment: 'Test-Comment', - assetId: 'test-asset', - ); - - when( - () => activityMock.addActivity( - 'test-album', - ActivityType.comment, - assetId: 'test-asset', - comment: 'Test-Comment', - ), - ).thenAnswer((_) async => AsyncError(Exception('Error'), StackTrace.current)); - - final albumProvider = albumActivityProvider('test-album'); - container.read(albumProvider.notifier); - await container.read(albumProvider.future); - - await container.read(provider.notifier).addComment('Test-Comment'); - - final activities = await container.read(provider.future); - expect(activities, hasLength(4)); - expect(activities, isNot(contains(comment))); - - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); - - final albumActivities = container.read(albumProvider).requireValue; - expect(albumActivities, hasLength(4)); - expect(albumActivities, isNot(contains(comment))); - }); - }); -} diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart deleted file mode 100644 index 7fe73868f5..0000000000 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/activity_statistics.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../test_utils.dart'; -import 'activity_mocks.dart'; - -void main() { - late ActivityServiceMock activityMock; - late ProviderContainer container; - late ListenerMock listener; - - setUp(() async { - activityMock = ActivityServiceMock(); - container = TestUtils.createContainer(overrides: [activityServiceProvider.overrideWith((ref) => activityMock)]); - listener = ListenerMock(); - }); - - test('Returns the proper count family', () async { - when( - () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 5)); - - // Read here to make the getStatistics call - container.read(activityStatisticsProvider('test-album', 'test-asset')); - - container.listen(activityStatisticsProvider('test-album', 'test-asset'), listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - verifyInOrder([() => listener.call(null, 0), () => listener.call(0, 5)]); - - verifyNoMoreInteractions(listener); - }); - - test('Adds activity', () async { - when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('test-album'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).addActivity(); - container.read(provider.notifier).addActivity(); - - expect(container.read(provider), 12); - }); - - test('Removes activity', () async { - when( - () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 10)); - - final provider = activityStatisticsProvider('new-album', 'test-asset'); - container.listen(provider, listener.call, fireImmediately: true); - - // Sleep for the getStatistics future to resolve - await Future.delayed(const Duration(milliseconds: 1)); - - container.read(provider.notifier).removeActivity(); - container.read(provider.notifier).removeActivity(); - - expect(container.read(provider), 8); - }); -} diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart deleted file mode 100644 index 4f4a2c7068..0000000000 --- a/mobile/test/modules/activity/activity_text_field_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../album/album_mocks.dart'; -import '../shared/shared_mocks.dart'; -import 'activity_mocks.dart'; - -void main() { - late Isar db; - late MockCurrentAlbumProvider mockCurrentAlbumProvider; - late MockAlbumActivity activityMock; - late List overrides; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - }); - - setUp(() { - mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); - activityMock = MockAlbumActivity(); - overrides = [ - currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), - albumActivityProvider(AlbumStub.twoAsset.remoteId!).overrideWith(() => activityMock), - ]; - }); - - testWidgets('Returns an Input text field', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(TextField), findsOneWidget); - }); - - testWidgets('No UserCircleAvatar when user == null', (tester) async { - final userProvider = MockCurrentUserProvider(); - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}), - overrides: [currentUserProvider.overrideWith((ref) => userProvider), ...overrides], - ); - - expect(find.byType(UserCircleAvatar), findsNothing); - }); - - testWidgets('UserCircleAvatar displayed when user != null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.byType(UserCircleAvatar), findsOneWidget); - }); - - testWidgets('Filled icon if likedId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: '1'), - overrides: overrides, - ); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing); - }); - - testWidgets('Bordered icon if likedId == null', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget); - expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing); - }); - - testWidgets('Adds new like', (tester) async { - await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides); - - when(() => activityMock.addLike()).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.addLike()); - }); - - testWidgets('Removes like if already liked', (tester) async { - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (_) {}, likeId: 'test-suffix'), - overrides: overrides, - ); - - when(() => activityMock.removeActivity(any())).thenAnswer((_) => Future.value()); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon); - - verify(() => activityMock.removeActivity('test-suffix')); - }); - - testWidgets('Passes text entered to onSubmit on submit', (tester) async { - String? receivedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receivedText = text, likeId: 'test-suffix'), - overrides: overrides, - ); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - expect(receivedText, 'This is a test comment'); - }); - - testWidgets('Input disabled when isEnabled false', (tester) async { - String? receviedText; - - await tester.pumpConsumerWidget( - ActivityTextField(onSubmit: (text) => receviedText = text, isEnabled: false, likeId: 'test-suffix'), - overrides: overrides, - ); - - final suffixIcon = find.byType(IconButton); - await tester.tap(suffixIcon, warnIfMissed: false); - - final textField = find.byType(TextField); - await tester.enterText(textField, 'This is a test comment'); - await tester.testTextInput.receiveAction(TextInputAction.done); - - expect(receviedText, isNull); - verifyNever(() => activityMock.addLike()); - verifyNever(() => activityMock.removeActivity(any())); - }); -} diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart deleted file mode 100644 index 538e3c0911..0000000000 --- a/mobile/test/modules/activity/activity_tile_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -import 'package:isar/isar.dart'; - -import '../../fixtures/asset.stub.dart'; -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - late Isar db; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - // For UserCircleAvatar - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, UserStub.admin); - await Store.put(StoreKey.serverEndpoint, ''); - await Store.put(StoreKey.accessToken, ''); - }); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a ListTile', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - expect(find.byType(ListTile), findsOneWidget); - }); - - testWidgets('No trailing widget when activity assetId == null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - testWidgets('Asset Thumbanil as trailing widget when activity assetId != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNotNull); - // TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class - }); - - testWidgets('No trailing widget when current asset != null', (tester) async { - await tester.pumpConsumerWidget( - ActivityTile( - Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'), - ), - overrides: overrides, - ); - - assetProvider.state = AssetStub.image1; - await tester.pumpAndSettle(); - - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.trailing, isNull); - }); - - group('Like Activity', () { - final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - - testWidgets('Like contains filled thumbs-up as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // And should have a thumb_up icon - final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up); - - expect(thumbUpIconFinder, findsOneWidget); - }); - - testWidgets('Like title is center aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.center); - }); - - testWidgets('No subtitle for likes', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNull); - }); - }); - - group('Comment Activity', () { - final activity = Activity( - id: '1', - createdAt: DateTime(100), - type: ActivityType.comment, - comment: 'This is a test comment', - user: UserStub.admin, - ); - - testWidgets('Comment contains User Circle Avatar as leading', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final userAvatarFinder = find.byType(UserCircleAvatar); - expect(userAvatarFinder, findsOneWidget); - - // Leading widget should not be null - final listTile = tester.widget(find.byType(ListTile)); - expect(listTile.leading, isNotNull); - - // Make sure that the leading widget is the UserCircleAvatar - final userAvatar = tester.widget(userAvatarFinder); - expect(listTile.leading, userAvatar); - }); - - testWidgets('Comment title is top aligned', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.titleAlignment, ListTileTitleAlignment.top); - }); - - testWidgets('Contains comment text as subtitle', (tester) async { - await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides); - - final listTile = tester.widget(find.byType(ListTile)); - - expect(listTile.subtitle, isNotNull); - expect(find.descendant(of: find.byType(ListTile), matching: find.text(activity.comment!)), findsOneWidget); - }); - }); -} diff --git a/mobile/test/modules/activity/dismissible_activity_test.dart b/mobile/test/modules/activity/dismissible_activity_test.dart deleted file mode 100644 index 32516e73ea..0000000000 --- a/mobile/test/modules/activity/dismissible_activity_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -@Tags(['widget']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; - -final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin); - -void main() { - late MockCurrentAssetProvider assetProvider; - late List overrides; - - setUpAll(() => TestUtils.init()); - - setUp(() { - assetProvider = MockCurrentAssetProvider(); - overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; - }); - - testWidgets('Returns a Dismissible', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - expect(find.byType(Dismissible), findsOneWidget); - }); - - testWidgets('Dialog displayed when onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byType(ConfirmDialog), findsOneWidget); - }); - - testWidgets('Ok action in ConfirmDialog should call onDismiss with activityId', (tester) async { - String? receivedActivityId; - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (id) => receivedActivityId = id), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(-500, 0)); - await tester.pumpAndSettle(); - - final okButton = find.text('delete'); - await tester.tap(okButton); - await tester.pumpAndSettle(); - - expect(receivedActivityId, '1'); - }); - - testWidgets('Delete icon for background if onDismiss is set', (tester) async { - await tester.pumpConsumerWidget( - DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), - overrides: overrides, - ); - - final dismissible = find.byType(Dismissible); - await tester.drag(dismissible, const Offset(500, 0)); - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget); - }); - - testWidgets('No delete dialog if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // When onDismiss is not set, the widget should not be wrapped by a Dismissible - expect(find.byType(Dismissible), findsNothing); - expect(find.byType(ConfirmDialog), findsNothing); - }); - - testWidgets('No icon for background if onDismiss is not set', (tester) async { - await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides); - - // No Dismissible should exist when onDismiss is not provided, so no delete icon either - expect(find.byType(Dismissible), findsNothing); - expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing); - }); -} diff --git a/mobile/test/modules/album/album_mocks.dart b/mobile/test/modules/album/album_mocks.dart deleted file mode 100644 index 7a1b76e0c7..0000000000 --- a/mobile/test/modules/album/album_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAlbumProvider extends CurrentAlbum with Mock implements CurrentAlbumInternal { - Album? initAlbum; - MockCurrentAlbumProvider([this.initAlbum]); - - @override - Album? build() { - return initAlbum; - } -} diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart deleted file mode 100644 index a35255bc21..0000000000 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../fixtures/album.stub.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../test_utils.dart'; -import '../settings/settings_mocks.dart'; - -void main() { - /// Verify the sort modes - group("AlbumSortMode", () { - late final Isar db; - - setUpAll(() async { - db = await TestUtils.initIsar(); - }); - - final albums = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - - setUp(() { - db.writeTxnSync(() { - db.clearSync(); - // Save all assets - db.assets.putAllSync([AssetStub.image1, AssetStub.image2]); - db.albums.putAllSync(albums); - for (final album in albums) { - album.sharedUsers.saveSync(); - album.assets.saveSync(); - } - }); - expect(db.albums.countSync(), 4); - expect(db.assets.countSync(), 2); - }); - - group("Album sort - Created Time", () { - const created = AlbumSortMode.created; - test("Created time - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created time - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Asset count", () { - const assetCount = AlbumSortMode.assetCount; - test("Asset Count - ASC", () { - final sorted = assetCount.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Asset Count - DESC", () { - final sorted = assetCount.sortFn(albums, true); - final sortedList = [AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Last modified", () { - const lastModified = AlbumSortMode.lastModified; - test("Last modified - ASC", () { - final sorted = lastModified.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Last modified - DESC", () { - final sorted = lastModified.sortFn(albums, true); - final sortedList = [AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Created", () { - const created = AlbumSortMode.created; - test("Created - ASC", () { - final sorted = created.sortFn(albums, false); - final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Created - DESC", () { - final sorted = created.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Recent", () { - const mostRecent = AlbumSortMode.mostRecent; - - test("Most Recent - DESC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], false); - final sortedList = [ - AlbumStub.create2020end2026Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2020Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Recent - ASC", () { - final sorted = mostRecent.sortFn([ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ], true); - final sortedList = [ - AlbumStub.create2020end2020Album, - AlbumStub.create2020end2022Album, - AlbumStub.create2020end2024Album, - AlbumStub.create2020end2026Album, - ]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - - group("Album sort - Most Oldest", () { - const mostOldest = AlbumSortMode.mostOldest; - - test("Most Oldest - ASC", () { - final sorted = mostOldest.sortFn(albums, false); - final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.oneAsset, AlbumStub.sharedWithUser]; - expect(sorted, orderedEquals(sortedList)); - }); - - test("Most Oldest - DESC", () { - final sorted = mostOldest.sortFn(albums, true); - final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.emptyAlbum, AlbumStub.twoAsset]; - expect(sorted, orderedEquals(sortedList)); - }); - }); - }); - - /// Verify the sort mode provider - group('AlbumSortByOptions', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort mode when none set', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created); - }); - - test('Returns the correct sort mode with index from Store', () { - // Returns the default value when nothing is set - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(3); - - expect(container.read(albumSortByOptionsProvider), AlbumSortMode.lastModified); - }); - - test('Properly saves the correct store index of sort mode', () { - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - verify( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, AlbumSortMode.mostOldest.storeIndex), - ); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0); - - final listener = ListenerMock(); - container.listen(albumSortByOptionsProvider, listener.call, fireImmediately: true); - - // Created -> Most Oldest - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest); - - // Most Oldest -> Title - container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.title); - - verifyInOrder([ - () => listener.call(null, AlbumSortMode.created), - () => listener.call(AlbumSortMode.created, AlbumSortMode.mostOldest), - () => listener.call(AlbumSortMode.mostOldest, AlbumSortMode.title), - ]); - - verifyNoMoreInteractions(listener); - }); - }); - - /// Verify the sort order provider - group('AlbumSortOrder', () { - late AppSettingsService settingsMock; - late ProviderContainer container; - - registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse); - - setUp(() async { - settingsMock = MockAppSettingsService(); - container = TestUtils.createContainer( - overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)], - ); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, any()), - ).thenAnswer((_) async => {}); - when( - () => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, any()), - ).thenAnswer((_) async => {}); - }); - - test('Returns the default sort order when none set - false', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - expect(container.read(albumSortOrderProvider), isFalse); - }); - - test('Properly saves the correct order', () { - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - verify(() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, true)); - }); - - test('Notifies listeners on state change', () { - when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false); - - final listener = ListenerMock(); - container.listen(albumSortOrderProvider, listener.call, fireImmediately: true); - - // false -> true - container.read(albumSortOrderProvider.notifier).changeSortDirection(true); - - // true -> false - container.read(albumSortOrderProvider.notifier).changeSortDirection(false); - - verifyInOrder([ - () => listener.call(null, false), - () => listener.call(false, true), - () => listener.call(true, false), - ]); - - verifyNoMoreInteractions(listener); - }); - }); -} diff --git a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart deleted file mode 100644 index 89b06d3c09..0000000000 --- a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentAssetProvider extends CurrentAssetInternal with Mock implements CurrentAsset { - Asset? initAsset; - MockCurrentAssetProvider([this.initAsset]); - - @override - Asset? build() { - return initAsset; - } -} diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart deleted file mode 100644 index 2b9b740ca7..0000000000 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:timezone/data/latest.dart'; -import 'package:timezone/timezone.dart'; - -ExifInfo makeExif({DateTime? dateTimeOriginal, String? timeZone}) { - return ExifInfo(dateTimeOriginal: dateTimeOriginal, timeZone: timeZone); -} - -Asset makeAsset({required String id, required DateTime createdAt, ExifInfo? exifInfo}) { - return Asset( - checksum: '', - localId: id, - remoteId: id, - ownerId: 1, - fileCreatedAt: createdAt, - fileModifiedAt: DateTime.now(), - updatedAt: DateTime.now(), - durationInSeconds: 0, - type: AssetType.image, - fileName: id, - isFavorite: false, - isArchived: false, - isTrashed: false, - exifInfo: exifInfo, - ); -} - -void main() { - // Init Timezone DB - initializeTimeZones(); - - group("Returns local time and offset if no exifInfo", () { - test('returns createdAt directly if in local', () { - final createdAt = DateTime(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - expect(createdAt, dt); - expect(createdAt.timeZoneOffset, tz); - }); - - test('returns createdAt in local if in utc', () { - final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12); - final a = makeAsset(id: '1', createdAt: createdAt); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final localCreatedAt = createdAt.toLocal(); - expect(localCreatedAt, dt); - expect(localCreatedAt.timeZoneOffset, tz); - }); - }); - - group("Returns dateTimeOriginal", () { - test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - - test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: "#_#"); // Invalid timezone - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final dateTimeInUTC = dateTimeOriginal.toUtc(); - expect(dateTimeInUTC, dt); - expect(dateTimeInUTC.timeZoneOffset, tz); - }); - }); - - group("Returns adjusted time if timezone available", () { - test('With timezone as location', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const location = "Asia/Hong_Kong"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final adjustedTime = TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location)); - expect(adjustedTime, dt); - expect(adjustedTime.timeZoneOffset, tz); - }); - - test('With timezone as offset', () { - final createdAt = DateTime.parse("2023-01-27T14:00:00-0500"); - final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530"); - const offset = "utc+08:00"; - final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset); - final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e); - final (dt, tz) = a.getTZAdjustedTimeAndOffset(); - - final location = getLocation("Asia/Hong_Kong"); - final offsetFromLocation = Duration(milliseconds: location.currentTimeZone.offset); - final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation); - - // Adds the offset to the actual time and returns the offset separately - expect(adjustedTime, dt); - expect(offsetFromLocation, tz); - }); - }); -} diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart deleted file mode 100644 index 3e1fe06c68..0000000000 --- a/mobile/test/modules/home/asset_grid_data_structure_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -void main() { - final List testAssets = []; - - for (int i = 0; i < 150; i++) { - int month = i ~/ 31; - int day = (i % 31).toInt(); - - DateTime date = DateTime(2022, month, day); - - testAssets.add( - Asset( - checksum: "", - localId: '$i', - ownerId: 1, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: '', - isFavorite: false, - isArchived: false, - isTrashed: false, - ), - ); - } - - final List assets = []; - - assets.addAll( - testAssets.sublist(0, 5).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 5); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(5, 10).map((e) { - e.fileCreatedAt = DateTime(2022, 1, 10); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(10, 15).map((e) { - e.fileCreatedAt = DateTime(2022, 2, 17); - return e; - }).toList(), - ); - assets.addAll( - testAssets.sublist(15, 30).map((e) { - e.fileCreatedAt = DateTime(2022, 10, 15); - return e; - }).toList(), - ); - - group('Test grouped', () { - test('test grouped check months', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 5 Rows - // Feb - // Day 1 - // 5 Assets => 2 Rows - // Jan - // Day 2 - // 5 Assets => 2 Rows - // Day 1 - // 5 Assets => 2 Rows - expect(renderList.elements, hasLength(4)); - expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[0].date.month, 1); - expect(renderList.elements[1].type, RenderAssetGridElementType.groupDividerTitle); - expect(renderList.elements[1].date.month, 1); - expect(renderList.elements[2].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[2].date.month, 2); - expect(renderList.elements[3].type, RenderAssetGridElementType.monthTitle); - expect(renderList.elements[3].date.month, 10); - }); - - test('test grouped check types', () async { - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day); - - // Oct - // Day 1 - // 15 Assets => 3 Rows - // Feb - // Day 1 - // 5 Assets => 1 Row - // Jan - // Day 2 - // 5 Assets => 1 Row - // Day 1 - // 5 Assets => 1 Row - final types = [ - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.groupDividerTitle, - RenderAssetGridElementType.monthTitle, - RenderAssetGridElementType.monthTitle, - ]; - - expect(renderList.elements, hasLength(types.length)); - - for (int i = 0; i < renderList.elements.length; i++) { - expect(renderList.elements[i].type, types[i]); - } - }); - }); -} diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index de16b7f24f..56efde98dd 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -2,16 +2,18 @@ @Tags(['widget']) library; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:isar/isar.dart'; import '../../test_utils.dart'; import '../../widget_tester_extensions.dart'; @@ -21,17 +23,17 @@ void main() { late MockMapStateNotifier mapStateNotifier; late List overrides; late MapState mapState; - late Isar db; + late Drift db; setUpAll(() async { - db = await TestUtils.initIsar(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); TestUtils.init(); }); setUp(() async { mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/modules/settings/settings_mocks.dart b/mobile/test/modules/settings/settings_mocks.dart deleted file mode 100644 index 63fd9312b7..0000000000 --- a/mobile/test/modules/settings/settings_mocks.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockAppSettingsService extends Mock implements AppSettingsService {} diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart deleted file mode 100644 index 790bbbd815..0000000000 --- a/mobile/test/modules/shared/shared_mocks.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { - MockCurrentUserProvider() : super(null); - - @override - set state(UserDto? user) => super.state = user; -} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart deleted file mode 100644 index 767a52b8d8..0000000000 --- a/mobile/test/modules/shared/sync_service_test.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../domain/service.mock.dart'; -import '../../fixtures/asset.stub.dart'; -import '../../infrastructure/repository.mock.dart'; -import '../../repository.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; - -void main() { - int assetIdCounter = 0; - Asset makeAsset({ - required String checksum, - String? localId, - String? remoteId, - int ownerId = 590700560494856554, // hash of "1" - }) { - final DateTime date = DateTime(2000); - return Asset( - id: assetIdCounter++, - checksum: checksum, - localId: localId, - remoteId: remoteId, - ownerId: ownerId, - fileCreatedAt: date, - fileModifiedAt: date, - updatedAt: date, - durationInSeconds: 0, - type: AssetType.image, - fileName: localId ?? remoteId ?? "", - isFavorite: false, - isArchived: false, - isTrashed: false, - ); - } - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime.now(), - ); - - setUpAll(() async { - final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - final LogRepository logRepository = LogRepository(loggerDb); - - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - await Store.put(StoreKey.currentUser, owner); - await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db)); - }); - - group('Test SyncService grouped', () { - final MockHashService hs = MockHashService(); - final MockEntityService entityService = MockEntityService(); - final MockAlbumRepository albumRepository = MockAlbumRepository(); - final MockAssetRepository assetRepository = MockAssetRepository(); - final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository(); - final MockIsarUserRepository userRepository = MockIsarUserRepository(); - final MockETagRepository eTagRepository = MockETagRepository(); - final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); - final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); - final MockAppSettingService appSettingService = MockAppSettingService(); - final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository(); - final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); - final MockUserApiRepository userApiRepository = MockUserApiRepository(); - final MockPartnerRepository partnerRepository = MockPartnerRepository(); - final MockUserService userService = MockUserService(); - - final owner = UserDto( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - name: "first last", - isAdmin: false, - profileChangedAt: DateTime(2021), - ); - - late SyncService s; - - final List initialAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", localId: "1", remoteId: "1-1"), - makeAsset(checksum: "d", localId: "2"), - makeAsset(checksum: "e", localId: "3"), - ]; - setUp(() { - s = SyncService( - hs, - entityService, - albumMediaRepository, - albumApiRepository, - albumRepository, - assetRepository, - exifInfoRepository, - partnerRepository, - userRepository, - userService, - eTagRepository, - appSettingService, - localFilesManagerRepository, - partnerApiRepository, - userApiRepository, - ); - when(() => userService.getMyUser()).thenReturn(owner); - when(() => eTagRepository.get(owner.id)).thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now())); - when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); - when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {}); - when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []); - when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]); - when(() => userRepository.getAll()).thenAnswer((_) async => [owner]); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => initialAssets); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[3], null, null]); - when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []); - when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {}); - when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]); - registerFallbackValue(Direction.sharedByMe); - when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []); - }); - test('test inserting existing assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isFalse); - verifyNever(() => assetRepository.updateAll(any())); - }); - - test('test inserting new assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "2-1"), - makeAsset(checksum: "c", remoteId: "1-1"), - makeAsset(checksum: "d", remoteId: "1-2"), - makeAsset(checksum: "f", remoteId: "1-4"), - makeAsset(checksum: "g", remoteId: "3-1"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]); - verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset])); - }); - - test('test syncing duplicate assets', () async { - final List remoteAssets = [ - makeAsset(checksum: "a", remoteId: "0-1"), - makeAsset(checksum: "b", remoteId: "1-1"), - makeAsset(checksum: "c", remoteId: "2-1"), - makeAsset(checksum: "h", remoteId: "2-1b"), - makeAsset(checksum: "i", remoteId: "2-1c"), - makeAsset(checksum: "j", remoteId: "2-1d"), - ]; - final bool c1 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c1, isTrue); - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => remoteAssets); - final bool c2 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c2, isFalse); - final currentState = [...remoteAssets]; - when( - () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum), - ).thenAnswer((_) async => currentState); - remoteAssets.removeAt(4); - final bool c3 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c3, isTrue); - remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e")); - remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2")); - final bool c4 = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: _failDiff, - loadAssets: (u, d) => remoteAssets, - ); - expect(c4, isTrue); - }); - - test('test efficient sync', () async { - when( - () => assetRepository.deleteAllByRemoteId([ - initialAssets[1].remoteId!, - initialAssets[2].remoteId!, - ], state: AssetState.remote), - ).thenAnswer((_) async { - return; - }); - when( - () => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged), - ).thenAnswer((_) async => [initialAssets[2]]); - when( - () => assetRepository.getAllByOwnerIdChecksum(any(), any()), - ).thenAnswer((_) async => [initialAssets[0], null, null]); //afg - final List toUpsert = [ - makeAsset(checksum: "a", remoteId: "0-1"), // changed - makeAsset(checksum: "f", remoteId: "0-2"), // new - makeAsset(checksum: "g", remoteId: "0-3"), // new - ]; - toUpsert[0].isFavorite = true; - final List toDelete = ["2-1", "1-1"]; - final expected = [...toUpsert]; - expected[0].id = initialAssets[0].id; - final bool c = await s.syncRemoteAssetsToDb( - users: [owner], - getChangedAssets: (user, since) async => (toUpsert, toDelete), - loadAssets: (user, date) => throw Exception(), - ); - expect(c, isTrue); - verify(() => assetRepository.updateAll(expected)); - }); - - group("upsertAssetsWithExif", () { - test('test upsert with EXIF data', () async { - final assets = [AssetStub.image1, AssetStub.image2]; - - expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null)); - await s.upsertAssetsWithExif(assets); - verify( - () => exifInfoRepository.updateAll( - any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))), - ), - ); - expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id)); - }); - }); - }); -} - -Future<(List?, List?)> _failDiff(List user, DateTime time) => Future.value((null, null)); diff --git a/mobile/test/modules/utils/migration_test.dart b/mobile/test/modules/utils/migration_test.dart deleted file mode 100644 index 08ab1204a6..0000000000 --- a/mobile/test/modules/utils/migration_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -void main() { - late Drift db; - late SyncStreamRepository mockSyncStreamRepository; - - setUpAll(() async { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); - mockSyncStreamRepository = MockSyncStreamRepository(); - when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {}); - }); - - tearDown(() async { - await Store.clear(); - }); - - group('handleBetaMigration Tests', () { - group("version < 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(14, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(14, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version == 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.needBetaMigration), true); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(15, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - - group("version > 15", () { - test('already on new timeline', () async { - await Store.put(StoreKey.betaTimeline, true); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('already on old timeline', () async { - await Store.put(StoreKey.betaTimeline, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), false); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - - test('fresh install', () async { - await Store.delete(StoreKey.betaTimeline); - await handleBetaMigration(16, true, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.betaTimeline), true); - expect(Store.tryGet(StoreKey.needBetaMigration), false); - }); - }); - }); - - group('sync reset tests', () { - test('version < 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(15, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), true); - }); - - test('version >= 16', () async { - await Store.put(StoreKey.shouldResetSync, false); - - await handleBetaMigration(16, false, mockSyncStreamRepository); - - expect(Store.tryGet(StoreKey.shouldResetSync), false); - }); - }); -} diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart index a577b0544f..18ab07b3a9 100644 --- a/mobile/test/modules/utils/openapi_patching_test.dart +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -21,7 +21,7 @@ void main() { """); upgradeDto(value, targetType); - expect(value['tags'], TagsResponse().toJson()); + expect(value['tags'], TagsResponse(enabled: false, sidebarWeb: false).toJson()); expect(value['download']['includeEmbeddedVideos'], false); }); diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart deleted file mode 100644 index 1757826daf..0000000000 --- a/mobile/test/modules/utils/throttler_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/utils/throttle.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; - -class _Counter { - int _count = 0; - _Counter(); - - int get count => _count; - void increment() { - dPrint(() => "Counter inside increment: $count"); - _count = _count + 1; - } -} - -void main() { - test('Executes the method immediately if no calls received previously', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 300)); - throttler.run(() => counter.increment()); - expect(counter.count, 1); - }); - - test('Does not execute calls before throttle interval', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 1); - }); - - test('Executes the method if received in intervals', () async { - var counter = _Counter(); - final throttler = Throttler(interval: const Duration(milliseconds: 100)); - for (final _ in Iterable.generate(10)) { - throttler.run(() => counter.increment()); - await Future.delayed(const Duration(milliseconds: 50)); - } - await Future.delayed(const Duration(seconds: 1)); - expect(counter.count, 5); - }); -} diff --git a/mobile/test/modules/utils/thumbnail_utils_test.dart b/mobile/test/modules/utils/thumbnail_utils_test.dart deleted file mode 100644 index dd4588fc80..0000000000 --- a/mobile/test/modules/utils/thumbnail_utils_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/utils/thumbnail_utils.dart'; - -void main() { - final dateTime = DateTime(2025, 04, 25, 12, 13, 14); - final dateTimeString = DateFormat.yMMMMd().format(dateTime); - - test('returns description if it has one', () { - final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []); - expect(result, 'description'); - }); - - test('returns image alt text with date if no location', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []); - expect(template, "image_alt_text_date"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - }); - - test('returns image alt text with date and place', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: 'city', country: 'country'), - dateTime, - AssetType.video, - [], - ); - expect(template, "image_alt_text_date_place"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - }); - - test('returns image alt text with date and some people', () { - final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]); - expect(template, "image_alt_text_date_2_people"); - expect(args["isVideo"], "false"); - expect(args["date"], dateTimeString); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - }); - - test('returns image alt text with date and location and many people', () { - final (template, args) = getAltTextTemplate( - const ExifInfo(city: "city", country: 'country'), - dateTime, - AssetType.video, - ["Alice", "Bob", "Carol", "David", "Eve"], - ); - expect(template, "image_alt_text_date_place_4_or_more_people"); - expect(args["isVideo"], "true"); - expect(args["date"], dateTimeString); - expect(args["city"], "city"); - expect(args["country"], "country"); - expect(args["person1"], "Alice"); - expect(args["person2"], "Bob"); - expect(args["person3"], "Carol"); - expect(args["additionalCount"], "2"); - }); -} diff --git a/mobile/test/pages/search/search.page_test.dart b/mobile/test/pages/search/search.page_test.dart deleted file mode 100644 index 9592623a28..0000000000 --- a/mobile/test/pages/search/search.page_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -@Skip('currently failing due to mock HTTP client to download ISAR binaries') -@Tags(['pages']) -library; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/pages/search/search.page.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../../dto.mocks.dart'; -import '../../service.mocks.dart'; -import '../../test_utils.dart'; -import '../../widget_tester_extensions.dart'; - -void main() { - late List overrides; - late Isar db; - late MockApiService mockApiService; - late MockSearchApi mockSearchApi; - - setUpAll(() async { - TestUtils.init(); - db = await TestUtils.initIsar(); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - mockApiService = MockApiService(); - mockSearchApi = MockSearchApi(); - when(() => mockApiService.searchApi).thenReturn(mockSearchApi); - registerFallbackValue(MockSmartSearchDto()); - registerFallbackValue(MockMetadataSearchDto()); - overrides = [ - dbProvider.overrideWithValue(db), - isarProvider.overrideWithValue(db), - apiServiceProvider.overrideWithValue(mockApiService), - ]; - }); - - final emptyTextSearch = isA().having((s) => s.originalFileName, 'originalFileName', null); - - testWidgets('contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.abc_rounded), findsOneWidget, reason: 'Should have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchSmart(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.query, 'query', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); - - testWidgets('not contextual search with/without text', (tester) async { - await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides); - - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key('contextual_search_button'))); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.image_search_rounded), findsOneWidget, reason: 'Should not have contextual search icon'); - - final searchField = find.byKey(const Key('search_text_field')); - expect(searchField, findsOneWidget); - - await tester.enterText(searchField, 'test'); - await tester.testTextInput.receiveAction(TextInputAction.search); - - var captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - - expect(captured.first, isA().having((s) => s.originalFileName, 'originalFileName', 'test')); - - await tester.enterText(searchField, ''); - await tester.testTextInput.receiveAction(TextInputAction.search); - - captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured; - expect(captured.first, emptyTextSearch); - }); -} diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 4b54ec4055..d049626f1d 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -1,48 +1,16 @@ -import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; -import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/album_api.repository.dart'; -import 'package:immich_mobile/repositories/partner.repository.dart'; -import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/repositories/file_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; -import 'package:immich_mobile/repositories/asset.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/repositories/album.repository.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockAlbumRepository extends Mock implements AlbumRepository {} - -class MockAssetRepository extends Mock implements AssetRepository {} - -class MockBackupRepository extends Mock implements BackupAlbumRepository {} - -class MockExifInfoRepository extends Mock implements IsarExifRepository {} - -class MockETagRepository extends Mock implements ETagRepository {} - -class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {} - -class MockBackupAlbumRepository extends Mock implements BackupAlbumRepository {} - class MockAssetApiRepository extends Mock implements AssetApiRepository {} class MockAssetMediaRepository extends Mock implements AssetMediaRepository {} -class MockFileMediaRepository extends Mock implements FileMediaRepository {} - -class MockAlbumApiRepository extends Mock implements AlbumApiRepository {} - class MockAuthApiRepository extends Mock implements AuthApiRepository {} class MockAuthRepository extends Mock implements AuthRepository {} -class MockPartnerRepository extends Mock implements PartnerRepository {} - -class MockPartnerApiRepository extends Mock implements PartnerApiRepository {} - class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 87a8c01cf0..4591dd845d 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,31 +1,10 @@ -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/backup.service.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/network.service.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; class MockApiService extends Mock implements ApiService {} -class MockAlbumService extends Mock implements AlbumService {} - -class MockBackupService extends Mock implements BackupService {} - -class MockSyncService extends Mock implements SyncService {} - -class MockHashService extends Mock implements HashService {} - -class MockEntityService extends Mock implements EntityService {} - class MockNetworkService extends Mock implements NetworkService {} -class MockSearchApi extends Mock implements SearchApi {} - class MockAppSettingService extends Mock implements AppSettingsService {} - -class MockBackgroundService extends Mock implements BackgroundService {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart deleted file mode 100644 index 97683cdab1..0000000000 --- a/mobile/test/services/album.service_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../domain/service.mock.dart'; -import '../fixtures/album.stub.dart'; -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -void main() { - late AlbumService sut; - late MockUserService userService; - late MockSyncService syncService; - late MockEntityService entityService; - late MockAlbumRepository albumRepository; - late MockAssetRepository assetRepository; - late MockBackupRepository backupRepository; - late MockAlbumMediaRepository albumMediaRepository; - late MockAlbumApiRepository albumApiRepository; - - setUp(() { - userService = MockUserService(); - syncService = MockSyncService(); - entityService = MockEntityService(); - albumRepository = MockAlbumRepository(); - assetRepository = MockAssetRepository(); - backupRepository = MockBackupRepository(); - albumMediaRepository = MockAlbumMediaRepository(); - albumApiRepository = MockAlbumApiRepository(); - - when(() => userService.getMyUser()).thenReturn(UserStub.user1); - - when( - () => albumRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - when( - () => assetRepository.transaction(any()), - ).thenAnswer((call) => (call.positionalArguments.first as Function).call()); - - sut = AlbumService( - syncService, - userService, - entityService, - albumRepository, - assetRepository, - backupRepository, - albumMediaRepository, - albumApiRepository, - ); - }); - - group('refreshDeviceAlbums', () { - test('empty selection with one album in db', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when(() => backupRepository.getIdsBySelection(BackupSelection.select)).thenAnswer((_) async => []); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => []); - when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1); - when(() => syncService.removeAllLocalAlbumsAndAssets()).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, false); - verify(() => syncService.removeAllLocalAlbumsAndAssets()); - }); - - test('one selected albums, two on device', () async { - when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []); - when( - () => backupRepository.getIdsBySelection(BackupSelection.select), - ).thenAnswer((_) async => [AlbumStub.oneAsset.localId!]); - when(() => albumMediaRepository.getAll()).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - when(() => syncService.syncLocalAlbumAssetsToDb(any(), any())).thenAnswer((_) async => true); - final result = await sut.refreshDeviceAlbums(); - expect(result, true); - verify(() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null)).called(1); - verifyNoMoreInteractions(syncService); - }); - }); - - group('refreshRemoteAlbums', () { - test('is working', () async { - when(() => syncService.getUsersFromServer()).thenAnswer((_) async => []); - when(() => syncService.syncUsersFromServer(any())).thenAnswer((_) async => true); - when(() => albumApiRepository.getAll(shared: true)).thenAnswer((_) async => [AlbumStub.sharedWithUser]); - - when( - () => albumApiRepository.getAll(shared: null), - ).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); - - when( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).thenAnswer((_) async => true); - final result = await sut.refreshRemoteAlbums(); - expect(result, true); - verify(() => syncService.getUsersFromServer()).called(1); - verify(() => syncService.syncUsersFromServer([])).called(1); - verify(() => albumApiRepository.getAll(shared: true)).called(1); - verify(() => albumApiRepository.getAll(shared: null)).called(1); - verify( - () => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]), - ).called(1); - verifyNoMoreInteractions(userService); - verifyNoMoreInteractions(albumApiRepository); - verifyNoMoreInteractions(syncService); - }); - }); - - group('createAlbum', () { - test('shared with assets', () async { - when( - () => albumApiRepository.create( - "name", - assetIds: any(named: "assetIds"), - sharedUserIds: any(named: "sharedUserIds"), - ), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when( - () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset), - ).thenAnswer((_) async => AlbumStub.oneAsset); - - when(() => albumRepository.create(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.twoAsset); - - final result = await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); - expect(result, AlbumStub.twoAsset); - verify( - () => albumApiRepository.create( - "name", - assetIds: [AssetStub.image1.remoteId!], - sharedUserIds: [UserStub.user1.id], - ), - ).called(1); - verify(() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset)).called(1); - }); - }); - - group('addAdditionalAssetToAlbum', () { - test('one added, one duplicate', () async { - when( - () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()), - ).thenAnswer((_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!])); - when(() => albumRepository.get(AlbumStub.oneAsset.id)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2])).thenAnswer((_) async {}); - when(() => albumRepository.removeAssets(AlbumStub.oneAsset, [])).thenAnswer((_) async {}); - when(() => albumRepository.recalculateMetadata(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - when(() => albumRepository.update(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset); - - final result = await sut.addAssets(AlbumStub.oneAsset, [AssetStub.image1, AssetStub.image2]); - - expect(result != null, true); - expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]); - expect(result.successfullyAdded, 1); - }); - }); - - group('addAdditionalUserToAlbum', () { - test('one added', () async { - when( - () => albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), - ).thenAnswer((_) async => AlbumStub.sharedWithUser); - - when( - () => albumRepository.addUsers( - AlbumStub.emptyAlbum, - AlbumStub.emptyAlbum.sharedUsers.map((u) => u.toDto()).toList(), - ), - ).thenAnswer((_) async => AlbumStub.emptyAlbum); - - when(() => albumRepository.update(AlbumStub.emptyAlbum)).thenAnswer((_) async => AlbumStub.emptyAlbum); - - final result = await sut.addUsers(AlbumStub.emptyAlbum, [UserStub.user2.id]); - - expect(result, true); - }); - }); -} diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart deleted file mode 100644 index b741150165..0000000000 --- a/mobile/test/services/asset.service_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; - -import '../api.mocks.dart'; -import '../domain/service.mock.dart'; -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; -import '../service.mocks.dart'; - -class FakeAssetBulkUpdateDto extends Fake implements AssetBulkUpdateDto {} - -void main() { - late AssetService sut; - - late MockAssetRepository assetRepository; - late MockAssetApiRepository assetApiRepository; - late MockExifInfoRepository exifInfoRepository; - late MockETagRepository eTagRepository; - late MockBackupAlbumRepository backupAlbumRepository; - late MockIsarUserRepository userRepository; - late MockAssetMediaRepository assetMediaRepository; - late MockApiService apiService; - - late MockSyncService syncService; - late MockAlbumService albumService; - late MockBackupService backupService; - late MockUserService userService; - - setUp(() { - assetRepository = MockAssetRepository(); - assetApiRepository = MockAssetApiRepository(); - exifInfoRepository = MockExifInfoRepository(); - userRepository = MockIsarUserRepository(); - eTagRepository = MockETagRepository(); - backupAlbumRepository = MockBackupAlbumRepository(); - apiService = MockApiService(); - assetMediaRepository = MockAssetMediaRepository(); - - syncService = MockSyncService(); - userService = MockUserService(); - albumService = MockAlbumService(); - backupService = MockBackupService(); - - sut = AssetService( - assetApiRepository, - assetRepository, - exifInfoRepository, - userRepository, - eTagRepository, - backupAlbumRepository, - apiService, - syncService, - backupService, - albumService, - userService, - assetMediaRepository, - ); - - registerFallbackValue(FakeAssetBulkUpdateDto()); - }); - - group("Edit ExifInfo", () { - late AssetsApi assetsApi; - setUp(() { - assetsApi = MockAssetsApi(); - when(() => apiService.assetsApi).thenReturn(assetsApi); - when(() => assetsApi.updateAssets(any())).thenAnswer((_) async => Future.value()); - }); - - test("asset is updated with DateTime", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final dateTime = DateTime.utc(2025, 6, 4, 2, 57); - await sut.changeDateTime(assets, dateTime.toIso8601String()); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedDatetime = receivedAssets.cast().map((a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0)); - expect(receivedDatetime.every((d) => d == dateTime), isTrue); - }); - - test("asset is updated with LatLng", () async { - final assets = [AssetStub.image1, AssetStub.image2]; - final latLng = const LatLng(37.7749, -122.4194); - await sut.changeLocation(assets, latLng); - - verify(() => assetsApi.updateAssets(any())).called(1); - final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny())); - upsertExifCallback.called(1); - final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; - final receivedCoords = receivedAssets.cast().map( - (a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), - ); - expect(receivedCoords.every((l) => l == latLng), isTrue); - }); - }); -} diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 7c7de3cd0e..f9a6d5e282 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -1,18 +1,19 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; -import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; import '../domain/service.mock.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; -import '../test_utils.dart'; void main() { late AuthService sut; @@ -22,7 +23,7 @@ void main() { late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; late MockAppSettingService appSettingsService; - late Isar db; + late Drift db; setUp(() async { authApiRepository = MockAuthApiRepository(); @@ -45,19 +46,16 @@ void main() { }); setUpAll(() async { - db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); + WidgetsFlutterBinding.ensureInitialized(); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + }); + + tearDownAll(() async { + await db.close(); }); group('validateServerUrl', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - final db = await TestUtils.initIsar(); - db.writeTxnSync(() => db.clearSync()); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - }); - test('Should resolve HTTP endpoint', () async { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart deleted file mode 100644 index 64b9fc604b..0000000000 --- a/mobile/test/services/entity.service_test.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/services/entity.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../fixtures/user.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; - -void main() { - late EntityService sut; - late MockAssetRepository assetRepository; - late MockIsarUserRepository userRepository; - - setUp(() { - assetRepository = MockAssetRepository(); - userRepository = MockIsarUserRepository(); - sut = EntityService(assetRepository, userRepository); - }); - - group('fillAlbumWithDatabaseEntities', () { - test('remote album with owner, thumbnail, sharedUsers and assets', () async { - final Album album = - Album( - name: "album-with-two-assets-and-two-users", - localId: "album-with-two-assets-and-two-users-local", - remoteId: "album-with-two-assets-and-two-users-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: true, - activityEnabled: true, - startDate: DateTime(2019), - endDate: DateTime(2020), - ) - ..remoteThumbnailAssetId = AssetStub.image1.remoteId - ..assets.addAll([AssetStub.image1, AssetStub.image1]) - ..owner.value = User.fromDto(UserStub.user1) - ..sharedUsers.addAll([User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)]); - - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin); - - when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)).thenAnswer((_) async => AssetStub.image1); - - when(() => userRepository.getByUserIds(any())).thenAnswer((_) async => [UserStub.user1, UserStub.user2]); - - when(() => assetRepository.getAllByRemoteId(any())).thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); - - await sut.fillAlbumWithDatabaseEntities(album); - expect(album.owner.value?.toDto(), UserStub.admin); - expect(album.thumbnail.value, AssetStub.image1); - expect(album.remoteUsers.map((u) => u.toDto()).toSet(), {UserStub.user1, UserStub.user2}); - expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2}); - }); - - test('remote album without any info', () async { - makeEmptyAlbum() => Album( - name: "album-without-info", - localId: "album-without-info-local", - remoteId: "album-without-info-remote", - createdAt: DateTime(2001), - modifiedAt: DateTime(2010), - shared: false, - activityEnabled: false, - ); - - final album = makeEmptyAlbum(); - await sut.fillAlbumWithDatabaseEntities(album); - verifyNoMoreInteractions(assetRepository); - verifyNoMoreInteractions(userRepository); - expect(album, makeEmptyAlbum()); - }); - }); -} diff --git a/mobile/test/services/hash_service_test.dart b/mobile/test/services/hash_service_test.dart deleted file mode 100644 index 9429d434b0..0000000000 --- a/mobile/test/services/hash_service_test.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:file/memory.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/device_asset.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/hash.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../service.mocks.dart'; -import '../mocks/asset_entity.mock.dart'; - -class MockAsset extends Mock implements Asset {} - -void main() { - late HashService sut; - late BackgroundService mockBackgroundService; - late IsarDeviceAssetRepository mockDeviceAssetRepository; - - setUp(() { - mockBackgroundService = MockBackgroundService(); - mockDeviceAssetRepository = MockDeviceAssetRepository(); - - sut = HashService(deviceAssetRepository: mockDeviceAssetRepository, backgroundService: mockBackgroundService); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - }); - when(() => mockDeviceAssetRepository.updateAll(any())).thenAnswer((_) async => true); - when(() => mockDeviceAssetRepository.deleteIds(any())).thenAnswer((_) async => true); - }); - - group("HashService: No DeviceAsset entry", () { - test("hash successfully", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - // No DB entries for this asset - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer((_) async => []); - - final result = await sut.hashAssets([mockAsset]); - - // Verify we stored the new hash in DB - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Has DeviceAsset entry", () { - test("when the asset is not modified", () async { - final hash = utf8.encode("image1-hash"); - - when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer( - (_) async => [ - DeviceAsset(assetId: AssetStub.image1.localId!, hash: hash, modifiedTime: AssetStub.image1.fileModifiedAt), - ], - ); - final result = await sut.hashAssets([AssetStub.image1]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - - test("hashed successful when asset is modified", () async { - final (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - - final result = await sut.hashAssets([mockAsset]); - - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - verify( - () => mockDeviceAssetRepository.updateAll([ - deviceAsset.copyWith(modifiedTime: AssetStub.image1.fileModifiedAt), - ]), - ).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([])).called(1); - }); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - - expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]); - }); - }); - - group("HashService: Cleanup", () { - late Asset mockAsset; - late Uint8List hash; - late DeviceAsset deviceAsset; - late File file; - - setUp(() async { - (mockAsset, file, deviceAsset, hash) = await _createAssetMock(AssetStub.image1); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]); - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]), - ).thenAnswer((_) async => [deviceAsset]); - }); - - test("cleanups DeviceAsset when local file cannot be obtained", () async { - when(() => mockAsset.local).thenThrow(Exception("File not found")); - final result = await sut.hashAssets([mockAsset]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockBackgroundService.digestFile(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - - expect(result, isEmpty); - }); - - test("cleanups DeviceAsset when hashing failed", () async { - when(() => mockDeviceAssetRepository.transaction(any())).thenAnswer((_) async { - final capturedCallback = verify(() => mockDeviceAssetRepository.transaction(captureAny())).captured; - // Invoke the transaction callback - await (capturedCallback.firstOrNull as Future Function()?)?.call(); - - // Verify the callback inside the transaction because, doing it outside results - // in a small delay before the callback is invoked, resulting in other LOCs getting executed - // resulting in an incorrect state - // - // i.e, consider the following piece of code - // await _deviceAssetRepository.transaction(() async { - // await _deviceAssetRepository.updateAll(toBeAdded); - // await _deviceAssetRepository.deleteIds(toBeDeleted); - // }); - // toBeDeleted.clear(); - // since the transaction method is mocked, the callback is not invoked until it is captured - // and executed manually in the next event loop. However, the toBeDeleted.clear() is executed - // immediately once the transaction stub is executed, resulting in the deleteIds method being - // called with an empty list. - // - // To avoid this, we capture the callback and execute it within the transaction stub itself - // and verify the results inside the transaction stub - verify(() => mockDeviceAssetRepository.updateAll([])).called(1); - verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1); - }); - - when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer( - // Invalid hash, length != 20 - (_) async => [Uint8List.fromList(hash.slice(2).toList())], - ); - - final result = await sut.hashAssets([mockAsset]); - - verify(() => mockBackgroundService.digestFiles([file.path])).called(1); - expect(result, isEmpty); - }); - }); - - group("HashService: Batch processing", () { - test("processes assets in batches when size limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - // Setup for multiple batch processing calls - when(() => mockBackgroundService.digestFiles([file1.path, file2.path])).thenAnswer((_) async => [hash1, hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - final size = await file1.length() + await file2.length(); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchSizeLimit: size, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path, file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("processes assets in batches when file limit is reached", () async { - // Setup multiple assets with large file sizes - final (mock1, mock2, mock3) = await ( - _createAssetMock(AssetStub.image1), - _createAssetMock(AssetStub.image2), - _createAssetMock(AssetStub.image3), - ).wait; - - final (asset1, file1, deviceAsset1, hash1) = mock1; - final (asset2, file2, deviceAsset2, hash2) = mock2; - final (asset3, file3, deviceAsset3, hash3) = mock3; - - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - when(() => mockBackgroundService.digestFiles([file1.path])).thenAnswer((_) async => [hash1]); - when(() => mockBackgroundService.digestFiles([file2.path])).thenAnswer((_) async => [hash2]); - when(() => mockBackgroundService.digestFiles([file3.path])).thenAnswer((_) async => [hash3]); - - sut = HashService( - deviceAssetRepository: mockDeviceAssetRepository, - backgroundService: mockBackgroundService, - batchFileLimit: 1, - ); - final result = await sut.hashAssets([asset1, asset2, asset3]); - - // Verify multiple batch process calls - verify(() => mockBackgroundService.digestFiles([file1.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file2.path])).called(1); - verify(() => mockBackgroundService.digestFiles([file3.path])).called(1); - - expect(result, [ - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - test("HashService: Sort & Process different states", () async { - final (asset1, file1, deviceAsset1, hash1) = await _createAssetMock(AssetStub.image1); // Will need rehashing - final (asset2, file2, deviceAsset2, hash2) = await _createAssetMock(AssetStub.image2); // Will have matching hash - final (asset3, file3, deviceAsset3, hash3) = await _createAssetMock(AssetStub.image3); // No DB entry - final asset4 = AssetStub.image3.copyWith(localId: "image4"); // Cannot be hashed - - when(() => mockBackgroundService.digestFiles([file1.path, file3.path])).thenAnswer((_) async => [hash1, hash3]); - // DB entries are not sorted and a dummy entry added - when( - () => mockDeviceAssetRepository.getByIds([ - AssetStub.image1.localId!, - AssetStub.image2.localId!, - AssetStub.image3.localId!, - asset4.localId!, - ]), - ).thenAnswer( - (_) async => [ - // Same timestamp to reuse deviceAsset - deviceAsset2.copyWith(modifiedTime: asset2.fileModifiedAt), - deviceAsset1, - deviceAsset3.copyWith(assetId: asset4.localId!), - ], - ); - - final result = await sut.hashAssets([asset1, asset2, asset3, asset4]); - - // Verify correct processing of all assets - verify(() => mockBackgroundService.digestFiles([file1.path, file3.path])).called(1); - expect(result.length, 3); - expect(result, [ - AssetStub.image2.copyWith(checksum: base64.encode(hash2)), - AssetStub.image1.copyWith(checksum: base64.encode(hash1)), - AssetStub.image3.copyWith(checksum: base64.encode(hash3)), - ]); - }); - - group("HashService: Edge cases", () { - test("handles empty list of assets", () async { - when(() => mockDeviceAssetRepository.getByIds(any())).thenAnswer((_) async => []); - - final result = await sut.hashAssets([]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - verifyNever(() => mockDeviceAssetRepository.deleteIds(any())); - - expect(result, isEmpty); - }); - - test("handles all file access failures", () async { - // No DB entries - when( - () => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!, AssetStub.image2.localId!]), - ).thenAnswer((_) async => []); - - final result = await sut.hashAssets([AssetStub.image1, AssetStub.image2]); - - verifyNever(() => mockBackgroundService.digestFiles(any())); - verifyNever(() => mockDeviceAssetRepository.updateAll(any())); - expect(result, isEmpty); - }); - }); - }); -} - -Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(Asset asset) async { - final random = Random(); - final hash = Uint8List.fromList(List.generate(20, (i) => random.nextInt(255))); - final mockAsset = MockAsset(); - final mockAssetEntity = MockAssetEntity(); - final fs = MemoryFileSystem(); - final deviceAsset = DeviceAsset( - assetId: asset.localId!, - hash: Uint8List.fromList(hash), - modifiedTime: DateTime.now(), - ); - final tmp = await fs.systemTempDirectory.createTemp(); - final file = tmp.childFile("${asset.fileName}-path"); - await file.writeAsString("${asset.fileName}-content"); - - when(() => mockAsset.localId).thenReturn(asset.localId); - when(() => mockAsset.fileName).thenReturn(asset.fileName); - when(() => mockAsset.fileCreatedAt).thenReturn(asset.fileCreatedAt); - when(() => mockAsset.fileModifiedAt).thenReturn(asset.fileModifiedAt); - when( - () => mockAsset.copyWith(checksum: any(named: "checksum")), - ).thenReturn(asset.copyWith(checksum: base64.encode(hash))); - when(() => mockAsset.local).thenAnswer((_) => mockAssetEntity); - when(() => mockAssetEntity.originFile).thenAnswer((_) async => file); - - return (mockAsset, file, deviceAsset, hash); -} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 30d4e2e6d4..75a41b46fb 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -4,82 +4,13 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as domain; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:mocktail/mocktail.dart'; import 'mock_http_override.dart'; -// Listener Mock to test when a provider notifies its listeners -class ListenerMock extends Mock { - void call(T? previous, T next); -} - abstract final class TestUtils { const TestUtils._(); - /// Downloads Isar binaries (if required) and initializes a new Isar db - static Future initIsar() async { - await Isar.initializeIsarCore(download: true); - - final instance = Isar.getInstance(); - if (instance != null) { - return instance; - } - - final db = await Isar.open( - [ - StoreValueSchema, - ExifInfoSchema, - AssetSchema, - AlbumSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - ETagSchema, - AndroidDeviceAssetSchema, - IOSDeviceAssetSchema, - DeviceAssetEntitySchema, - ], - directory: "test/", - maxSizeMiB: 1024, - inspector: false, - ); - - // Clear and close db on test end - addTearDown(() async { - await db.writeTxn(() async => await db.clear()); - await db.close(); - }); - return db; - } - - /// Creates a new ProviderContainer to test Riverpod providers - static ProviderContainer createContainer({ - ProviderContainer? parent, - List overrides = const [], - List? observers, - }) { - final container = ProviderContainer(parent: parent, overrides: overrides, observers: observers); - - // Dispose on test end - addTearDown(container.dispose); - - return container; - } - static void init() { // Turn off easy localization logging EasyLocalization.logger.enableBuildModes = []; diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 50e73e5b5e..c8c41bbf0f 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; @@ -10,28 +9,6 @@ class MediumFactory { const MediumFactory(Drift db) : _db = db; - LocalAsset localAsset({ - String? id, - String? name, - AssetType? type, - DateTime? createdAt, - DateTime? updatedAt, - String? checksum, - }) { - final random = Random(); - - return LocalAsset( - id: id ?? '${random.nextInt(1000000)}', - name: name ?? 'Asset ${random.nextInt(1000000)}', - checksum: checksum ?? '${random.nextInt(1000000)}', - type: type ?? AssetType.image, - createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), - playbackStyle: AssetPlaybackStyle.image, - isEdited: false, - ); - } - LocalAlbum localAlbum({ String? id, String? name, diff --git a/mobile/test/utils/editor_test.dart b/mobile/test/utils/editor_test.dart new file mode 100644 index 0000000000..16f1c08d05 --- /dev/null +++ b/mobile/test/utils/editor_test.dart @@ -0,0 +1,322 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/utils/editor.utils.dart'; +import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; + +List normalizedToEdits(NormalizedTransform transform) { + List edits = []; + + if (transform.mirrorHorizontal) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); + } + + if (transform.mirrorVertical) { + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); + } + + if (transform.rotation != 0) { + edits.add(RotateEdit(RotateParameters(angle: transform.rotation))); + } + + return edits; +} + +bool compareEditAffines(List editsA, List editsB) { + final normA = buildAffineFromEdits(editsA); + final normB = buildAffineFromEdits(editsB); + + return ((normA.a - normB.a).abs() < 0.0001 && + (normA.b - normB.b).abs() < 0.0001 && + (normA.c - normB.c).abs() < 0.0001 && + (normA.d - normB.d).abs() < 0.0001); +} + +void main() { + group('normalizeEdits', () { + test('should handle no edits', () { + final edits = []; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 90° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 180° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single 270° rotation', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single horizontal mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle a single vertical mirror', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 90° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 180° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + horizontal mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + vertical mirror', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle 270° rotation + both mirrors', () { + final edits = [ + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle horizontal mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle vertical mirror + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 90° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 180° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + + test('should handle both mirrors + 270° rotation', () { + final edits = [ + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), + ]; + + final result = normalizeTransformEdits(edits); + final normalizedEdits = normalizedToEdits(result); + + expect(compareEditAffines(normalizedEdits, edits), true); + }); + }); +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 522063185f..9d7b158fc3 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash OPENAPI_GENERATOR_VERSION=v7.12.0 +set -euo pipefail + # usage: ./bin/generate-open-api.sh function dart { @@ -15,12 +17,13 @@ function dart { patch --no-backup-if-mismatch -u api.mustache