diff --git a/.gitattributes b/.gitattributes index 2e8a45ca5c3..3d43ff20ed9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,9 @@ mobile/lib/**/*.g.dart linguist-generated=true mobile/lib/**/*.drift.dart -diff -merge mobile/lib/**/*.drift.dart linguist-generated=true +mobile/drift_schemas/main/drift_schema_*.json -diff -merge +mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true + open-api/typescript-sdk/fetch-client.ts -diff -merge open-api/typescript-sdk/fetch-client.ts linguist-generated=true diff --git a/.github/actions/image-build/action.yml b/.github/actions/image-build/action.yml deleted file mode 100644 index a4168dcd5a7..00000000000 --- a/.github/actions/image-build/action.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: 'Single arch image build' -description: 'Build single-arch image on platform appropriate runner' -inputs: - image: - description: 'Name of the image to build' - required: true - ghcr-token: - description: 'GitHub Container Registry token' - required: true - platform: - description: 'Platform to build for' - required: true - artifact-key-base: - description: 'Base key for artifact name' - required: true - context: - description: 'Path to build context' - required: true - dockerfile: - description: 'Path to Dockerfile' - required: true - build-args: - description: 'Docker build arguments' - required: false -runs: - using: 'composite' - steps: - - name: Prepare - id: prepare - shell: bash - env: - PLATFORM: ${{ inputs.platform }} - run: | - echo "platform-pair=${PLATFORM//\//-}" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Login to GitHub Container Registry - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ inputs.ghcr-token }} - - - name: Generate cache key suffix - id: cache-key-suffix - shell: bash - env: - REF: ${{ github.ref_name }} - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "cache-key-suffix=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT - else - SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') - echo "suffix=${SUFFIX}" >> $GITHUB_OUTPUT - fi - - - name: Generate cache target - id: cache-target - shell: bash - env: - BUILD_ARGS: ${{ inputs.build-args }} - IMAGE: ${{ inputs.image }} - SUFFIX: ${{ steps.cache-key-suffix.outputs.suffix }} - PLATFORM_PAIR: ${{ steps.prepare.outputs.platform-pair }} - run: | - HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1) - CACHE_KEY="${PLATFORM_PAIR}-${HASH}" - echo "cache-key-base=${CACHE_KEY}" >> $GITHUB_OUTPUT - if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then - # Essentially just ignore the cache output (forks can't write to registry cache) - echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT - else - echo "cache-to=type=registry,ref=${IMAGE}-build-cache:${CACHE_KEY}-${SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT - fi - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - - - name: Build and push image - id: build - uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 - with: - context: ${{ inputs.context }} - file: ${{ inputs.dockerfile }} - platforms: ${{ inputs.platform }} - labels: ${{ steps.meta.outputs.labels }} - cache-to: ${{ steps.cache-target.outputs.cache-to }} - cache-from: | - type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-${{ steps.cache-key-suffix.outputs.suffix }} - type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-main - outputs: type=image,"name=${{ inputs.image }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} - build-args: | - BUILD_ID=${{ github.run_id }} - BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.meta.outputs.tags }} - BUILD_SOURCE_REF=${{ github.ref_name }} - BUILD_SOURCE_COMMIT=${{ github.sha }} - ${{ inputs.build-args }} - - - name: Export digest - shell: bash - run: | # zizmor: ignore[template-injection] - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: ${{ inputs.artifact-key-base }}-${{ steps.cache-target.outputs.cache-key-base }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 0fcc4f1d7c9..33912d687cb 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -93,6 +93,10 @@ jobs: run: make translation working-directory: ./mobile + - name: Generate platform APIs + run: make pigeon + working-directory: ./mobile + - name: Build Android App Bundle working-directory: ./mobile env: diff --git a/.github/workflows/multi-runner-build.yml b/.github/workflows/multi-runner-build.yml deleted file mode 100644 index f6d7c123552..00000000000 --- a/.github/workflows/multi-runner-build.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: 'Multi-runner container image build' -on: - workflow_call: - inputs: - image: - description: 'Name of the image' - type: string - required: true - context: - description: 'Path to build context' - type: string - required: true - dockerfile: - description: 'Path to Dockerfile' - type: string - required: true - tag-suffix: - description: 'Suffix to append to the image tag' - type: string - default: '' - dockerhub-push: - description: 'Push to Docker Hub' - type: boolean - default: false - build-args: - description: 'Docker build arguments' - type: string - required: false - platforms: - description: 'Platforms to build for' - type: string - runner-mapping: - description: 'Mapping from platforms to runners' - type: string - secrets: - DOCKERHUB_USERNAME: - required: false - DOCKERHUB_TOKEN: - required: false - -env: - GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ inputs.image }} - DOCKERHUB_IMAGE: altran1502/${{ inputs.image }} - -jobs: - matrix: - name: 'Generate matrix' - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - key: ${{ steps.artifact-key.outputs.base }} - steps: - - name: Generate build matrix - id: matrix - shell: bash - env: - PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }} - RUNNER_MAPPING: ${{ inputs.runner-mapping || '{"linux/amd64":"ubuntu-latest","linux/arm64":"ubuntu-24.04-arm"}' }} - run: | - matrix=$(jq -R -c \ - --argjson runner_mapping "${RUNNER_MAPPING}" \ - 'split(",") | map({platform: ., runner: $runner_mapping[.]})' \ - <<< "${PLATFORMS}") - echo "${matrix}" - echo "matrix=${matrix}" >> $GITHUB_OUTPUT - - - name: Determine artifact key - id: artifact-key - shell: bash - env: - IMAGE: ${{ inputs.image }} - SUFFIX: ${{ inputs.tag-suffix }} - run: | - if [[ -n "${SUFFIX}" ]]; then - base="${IMAGE}${SUFFIX}-digests" - else - base="${IMAGE}-digests" - fi - echo "${base}" - echo "base=${base}" >> $GITHUB_OUTPUT - - build: - needs: matrix - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.matrix.outputs.matrix) }} - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - persist-credentials: false - - - uses: ./.github/actions/image-build - with: - context: ${{ inputs.context }} - dockerfile: ${{ inputs.dockerfile }} - image: ${{ env.GHCR_IMAGE }} - ghcr-token: ${{ secrets.GITHUB_TOKEN }} - platform: ${{ matrix.platform }} - artifact-key-base: ${{ needs.matrix.outputs.key }} - build-args: ${{ inputs.build-args }} - - merge: - needs: [matrix, build] - runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.head.repo.fork }} - permissions: - contents: read - actions: read - packages: write - steps: - - name: Download digests - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - path: ${{ runner.temp }}/digests - pattern: ${{ needs.matrix.outputs.key }}-* - merge-multiple: true - - - name: Login to Docker Hub - if: ${{ inputs.dockerhub-push }} - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GHCR - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - with: - flavor: | - # Disable latest tag - latest=false - suffix=${{ inputs.tag-suffix }} - images: | - name=${{ env.GHCR_IMAGE }} - name=${{ env.DOCKERHUB_IMAGE }},enable=${{ inputs.dockerhub-push }} - tags: | - # Tag with branch name - type=ref,event=branch - # Tag with pr-number - type=ref,event=pr - # Tag with long commit sha hash - type=sha,format=long,prefix=commit- - # Tag with git tag on release - type=ref,event=tag - type=raw,value=release,enable=${{ github.event_name == 'release' }} - - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - # Process annotations - declare -a ANNOTATIONS=() - if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then - while IFS= read -r annotation; do - # Extract key and value by removing the manifest: prefix - if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then - key="${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - # Use array to properly handle arguments with spaces - ANNOTATIONS+=(--annotation "index:$key=$value") - fi - done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") - fi - - TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - SOURCE_ARGS=$(printf "${GHCR_IMAGE}@sha256:%s " *) - - docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 7cd28228dc2..754c0c38b31 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -59,13 +59,17 @@ jobs: working-directory: ./mobile - name: Generate translation file - run: make translation; dart format lib/generated/codegen_loader.g.dart + run: make translation working-directory: ./mobile - name: Run Build Runner run: make build working-directory: ./mobile + - name: Generate platform API + run: make pigeon + working-directory: ./mobile + - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files diff --git a/cli/package-lock.json b/cli/package-lock.json index 5373f3cdd15..9464f866630 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1370,17 +1370,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1394,7 +1394,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1410,16 +1410,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -1434,15 +1434,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1452,15 +1453,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1477,9 +1513,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -1491,14 +1527,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1544,16 +1582,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1568,13 +1606,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2756,9 +2794,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -4200,15 +4238,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index d06b30e984e..edb24e2f671 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -43,6 +43,7 @@ export interface UploadOptionsDto { concurrency: number; progress?: boolean; watch?: boolean; + jsonOutput?: boolean; } class UploadFile extends File { @@ -65,6 +66,9 @@ class UploadFile extends File { const uploadBatch = async (files: string[], options: UploadOptionsDto) => { const { newFiles, duplicates } = await checkForDuplicates(files, options); const newAssets = await uploadFiles(newFiles, options); + if (options.jsonOutput) { + console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4)); + } await updateAlbums([...newAssets, ...duplicates], options); await deleteFiles(newFiles, options); }; diff --git a/cli/src/index.ts b/cli/src/index.ts index 5da4b50722b..a0392186c09 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -68,6 +68,11 @@ program .env('IMMICH_UPLOAD_CONCURRENCY') .default(4), ) + .addOption( + new Option('-j, --json-output', 'Output detailed information in json format') + .env('IMMICH_JSON_OUTPUT') + .default(false), + ) .addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS')) .addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true)) .addOption( diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 7e55e4e88ff..7b90c3b19ab 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -219,3 +219,10 @@ When you turn off the storage template engine, it will leave the assets in `UPLO Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files. You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface. ::: + +## Backup ordering + +A backup of Immich should contain both the database and the asset files. When backing these up it's possible for them to get out of sync, potentially resulting in broken assets after you restore. +The best way of dealing with this is to stop the immich-server container while you take a backup. If nothing is changing then the backup will always be in sync. + +If stopping the container is not an option, then the recommended order is to back up the database first, and the filesystem second. This way, the worst case scenario is that there are files on the filesystem that the database doesn't know about. If necessary, these can be (re)uploaded manually after a restore. If the backup is done the other way around, with the filesystem first and the database second, it's possible for the restored database to reference files that aren't in the filesystem backup, thus resulting in broken assets. diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 2dc69909446..b60b5dbb8bf 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -93,6 +93,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a ## Auto Launch When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`. +Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich. ## Mobile Redirect URI diff --git a/docs/docs/features/casting.md b/docs/docs/features/casting.md index cc25e24da76..bca85cb28ce 100644 --- a/docs/docs/features/casting.md +++ b/docs/docs/features/casting.md @@ -2,6 +2,14 @@ Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future. +## Enable Google Cast Support + +Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in. + +You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast` + + + ## Limitations To use casting with Immich, there are a few prerequisites: diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md index 1ab79ee3f19..436b499e505 100644 --- a/docs/docs/features/command-line-interface.md +++ b/docs/docs/features/command-line-interface.md @@ -90,19 +90,22 @@ Usage: immich upload [paths...] [options] Upload assets Arguments: -paths One or more paths to assets to be uploaded + paths One or more paths to assets to be uploaded Options: --r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) --i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS) --h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) --H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN) --a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) --A, --album-name Add all assets to specified album (env: IMMICH_ALBUM_NAME) --n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) --c, --concurrency Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY) ---delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) ---help display help for command + -r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) + -i, --ignore Pattern to ignore (env: IMMICH_IGNORE_PATHS) + -h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) + -H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN) + -a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) + -A, --album-name Add all assets to specified album (env: IMMICH_ALBUM_NAME) + -n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) + -c, --concurrency Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY) + -j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT) + --delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) + --no-progress Hide progress bars (env: IMMICH_PROGRESS_BAR) + --watch Watch for changes and upload automatically (default: false, env: IMMICH_WATCH_CHANGES) + --help display help for command ``` @@ -172,6 +175,16 @@ By default, hidden files are skipped. If you want to include hidden files, use t immich upload --include-hidden --recursive directory/ ``` +You can use the `--json-output` option to get a json printed which includes +three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging +output you will need to strip the first three lines of output to get the json. +For example to get a list of files that would be uploaded for further +processing: + +```bash +immich upload --dry-run . | tail -n +4 | jq .newFiles[] +``` + ### Obtain the API Key The API key can be obtained in the user setting panel on the web interface. diff --git a/docs/docs/features/img/gcast-enable.webp b/docs/docs/features/img/gcast-enable.webp new file mode 100644 index 00000000000..f128b82e25c Binary files /dev/null and b/docs/docs/features/img/gcast-enable.webp differ diff --git a/docs/package-lock.json b/docs/package-lock.json index 602232da074..ce1ba4cd9d1 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,8 +8,8 @@ "name": "documentation", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "~3.7.0", - "@docusaurus/preset-classic": "~3.7.0", + "@docusaurus/core": "~3.8.0", + "@docusaurus/preset-classic": "~3.8.0", "@mdi/js": "^7.3.67", "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", @@ -27,7 +27,7 @@ "url": "^0.11.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "~3.7.0", + "@docusaurus/module-type-aliases": "~3.8.0", "@docusaurus/tsconfig": "^3.7.0", "@docusaurus/types": "^3.7.0", "prettier": "^3.2.4", @@ -3128,9 +3128,9 @@ } }, "node_modules/@docusaurus/babel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.7.0.tgz", - "integrity": "sha512-0H5uoJLm14S/oKV3Keihxvh8RV+vrid+6Gv+2qhuzbqHanawga8tYnsdpjEyt36ucJjqlby2/Md2ObWjA02UXQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.8.0.tgz", + "integrity": "sha512-9EJwSgS6TgB8IzGk1L8XddJLhZod8fXT4ULYMx6SKqyCBqCFpVCEjR/hNXXhnmtVM2irDuzYoVLGWv7srG/VOA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", @@ -3143,8 +3143,8 @@ "@babel/runtime": "^7.25.9", "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/utils": "3.8.0", "babel-plugin-dynamic-import-node": "^2.3.3", "fs-extra": "^11.1.1", "tslib": "^2.6.0" @@ -3154,17 +3154,17 @@ } }, "node_modules/@docusaurus/bundler": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.7.0.tgz", - "integrity": "sha512-CUUT9VlSGukrCU5ctZucykvgCISivct+cby28wJwCC/fkQFgAHRp/GKv2tx38ZmXb7nacrKzFTcp++f9txUYGg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.8.0.tgz", + "integrity": "sha512-Rq4Z/MSeAHjVzBLirLeMcjLIAQy92pF1OI+2rmt18fSlMARfTGLWRE8Vb+ljQPTOSfJxwDYSzsK6i7XloD2rNA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.7.0", - "@docusaurus/cssnano-preset": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/babel": "3.8.0", + "@docusaurus/cssnano-preset": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", "babel-loader": "^9.2.1", "clean-css": "^5.3.2", "copy-webpack-plugin": "^11.0.0", @@ -3178,7 +3178,6 @@ "postcss": "^8.4.26", "postcss-loader": "^7.3.3", "postcss-preset-env": "^10.1.0", - "react-dev-utils": "^12.0.1", "terser-webpack-plugin": "^5.3.9", "tslib": "^2.6.0", "url-loader": "^4.1.1", @@ -3198,18 +3197,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz", - "integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.8.0.tgz", + "integrity": "sha512-c7u6zFELmSGPEP9WSubhVDjgnpiHgDqMh1qVdCB7rTflh4Jx0msTYmMiO91Ez0KtHj4sIsDsASnjwfJ2IZp3Vw==", "license": "MIT", "dependencies": { - "@docusaurus/babel": "3.7.0", - "@docusaurus/bundler": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/babel": "3.8.0", + "@docusaurus/bundler": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "boxen": "^6.2.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -3217,19 +3216,19 @@ "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", - "del": "^6.1.1", "detect-port": "^1.5.1", "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", + "execa": "5.1.1", "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", "leven": "^3.1.0", "lodash": "^4.17.21", + "open": "^8.4.0", "p-map": "^4.0.0", "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", "react-loadable-ssr-addon-v5-slorber": "^1.0.1", @@ -3238,7 +3237,7 @@ "react-router-dom": "^5.3.4", "semver": "^7.5.4", "serve-handler": "^6.1.6", - "shelljs": "^0.8.5", + "tinypool": "^1.0.2", "tslib": "^2.6.0", "update-notifier": "^6.0.2", "webpack": "^5.95.0", @@ -3259,9 +3258,9 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.7.0.tgz", - "integrity": "sha512-X9GYgruZBSOozg4w4dzv9uOz8oK/EpPVQXkp0MM6Tsgp/nRIU9hJzJ0Pxg1aRa3xCeEQTOimZHcocQFlLwYajQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.0.tgz", + "integrity": "sha512-UJ4hAS2T0R4WNy+phwVff2Q0L5+RXW9cwlH6AEphHR5qw3m/yacfWcSK7ort2pMMbDn8uGrD38BTm4oLkuuNoQ==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", @@ -3274,9 +3273,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.7.0.tgz", - "integrity": "sha512-z7g62X7bYxCYmeNNuO9jmzxLQG95q9QxINCwpboVcNff3SJiHJbGrarxxOVMVmAh1MsrSfxWkVGv4P41ktnFsA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.8.0.tgz", + "integrity": "sha512-7eEMaFIam5Q+v8XwGqF/n0ZoCld4hV4eCCgQkfcN9Mq5inoZa6PHHW9Wu6lmgzoK5Kx3keEeABcO2SxwraoPDQ==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -3287,21 +3286,21 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.7.0.tgz", - "integrity": "sha512-OFBG6oMjZzc78/U3WNPSHs2W9ZJ723ewAcvVJaqS0VgyeUfmzUV8f1sv+iUHA0DtwiR5T5FjOxj6nzEE8LY6VA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.8.0.tgz", + "integrity": "sha512-mDPSzssRnpjSdCGuv7z2EIAnPS1MHuZGTaRLwPn4oQwszu4afjWZ/60sfKjTnjBjI8Vl4OgJl2vMmfmiNDX4Ng==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", "estree-util-value-to-estree": "^3.0.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", - "image-size": "^1.0.2", + "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "rehype-raw": "^7.0.0", @@ -3326,17 +3325,17 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz", - "integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.0.tgz", + "integrity": "sha512-/uMb4Ipt5J/QnD13MpnoC/A4EYAe6DKNWqTWLlGrqsPJwJv73vSwkA25xnYunwfqWk0FlUQfGv/Swdh5eCCg7g==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.7.0", + "@docusaurus/types": "3.8.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" }, "peerDependencies": { @@ -3345,24 +3344,24 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.7.0.tgz", - "integrity": "sha512-EFLgEz6tGHYWdPU0rK8tSscZwx+AsyuBW/r+tNig2kbccHYGUJmZtYN38GjAa3Fda4NU+6wqUO5kTXQSRBQD3g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.0.tgz", + "integrity": "sha512-0SlOTd9R55WEr1GgIXu+hhTT0hzARYx3zIScA5IzpdekZQesI/hKEa5LPHBd415fLkWMjdD59TaW/3qQKpJ0Lg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "cheerio": "1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^11.1.1", "lodash": "^4.17.21", - "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", @@ -3379,25 +3378,26 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz", - "integrity": "sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.0.tgz", + "integrity": "sha512-fRDMFLbUN6eVRXcjP8s3Y7HpAt9pzPYh1F/7KKXOCxvJhjjCtbon4VJW0WndEPInVz4t8QUXn5QZkU2tGVCE2g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/module-type-aliases": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "schema-dts": "^1.1.2", "tslib": "^2.6.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" @@ -3411,16 +3411,16 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz", - "integrity": "sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.0.tgz", + "integrity": "sha512-39EDx2y1GA0Pxfion5tQZLNJxL4gq6susd1xzetVBjVIQtwpCdyloOfQBAgX0FylqQxfJrYqL0DIUuq7rd7uBw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" @@ -3433,17 +3433,32 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/plugin-debug": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.7.0.tgz", - "integrity": "sha512-Qgg+IjG/z4svtbCNyTocjIwvNTNEwgRjSXXSJkKVG0oWoH0eX/HAPiu+TS1HBwRPQV+tTYPWLrUypYFepfujZA==", + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.0.tgz", + "integrity": "sha512-/VBTNymPIxQB8oA3ZQ4GFFRYdH4ZxDRRBECxyjRyv486mfUPXfcdk+im4S5mKWa6EK2JzBz95IH/Wu0qQgJ5yQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.8.0.tgz", + "integrity": "sha512-teonJvJsDB9o2OnG6ifbhblg/PXzZvpUKHFgD8dOL1UJ58u0lk8o0ZOkvaYEBa9nDgqzoWrRk9w+e3qaG2mOhQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", "fs-extra": "^11.1.1", - "react-json-view-lite": "^1.2.0", + "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" }, "engines": { @@ -3455,14 +3470,14 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.7.0.tgz", - "integrity": "sha512-otIqiRV/jka6Snjf+AqB360XCeSv7lQC+DKYW+EUZf6XbuE8utz5PeUQ8VuOcD8Bk5zvT1MC4JKcd5zPfDuMWA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.0.tgz", + "integrity": "sha512-aKKa7Q8+3xRSRESipNvlFgNp3FNPELKhuo48Cg/svQbGNwidSHbZT03JqbW4cBaQnyyVchO1ttk+kJ5VC9Gx0w==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "tslib": "^2.6.0" }, "engines": { @@ -3474,14 +3489,14 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.7.0.tgz", - "integrity": "sha512-M3vrMct1tY65ModbyeDaMoA+fNJTSPe5qmchhAbtqhDD/iALri0g9LrEpIOwNaoLmm6lO88sfBUADQrSRSGSWA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.0.tgz", + "integrity": "sha512-ugQYMGF4BjbAW/JIBtVcp+9eZEgT9HRdvdcDudl5rywNPBA0lct+lXMG3r17s02rrhInMpjMahN3Yc9Cb3H5/g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, @@ -3494,14 +3509,14 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.7.0.tgz", - "integrity": "sha512-X8U78nb8eiMiPNg3jb9zDIVuuo/rE1LjGDGu+5m5CX4UBZzjMy+klOY2fNya6x8ACyE/L3K2erO1ErheP55W/w==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.0.tgz", + "integrity": "sha512-9juRWxbwZD3SV02Jd9QB6yeN7eu+7T4zB0bvJLcVQwi+am51wAxn2CwbdL0YCCX+9OfiXbADE8D8Q65Hbopu/w==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "tslib": "^2.6.0" }, "engines": { @@ -3513,17 +3528,17 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.7.0.tgz", - "integrity": "sha512-bTRT9YLZ/8I/wYWKMQke18+PF9MV8Qub34Sku6aw/vlZ/U+kuEuRpQ8bTcNOjaTSfYsWkK4tTwDMHK2p5S86cA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.0.tgz", + "integrity": "sha512-fGpOIyJvNiuAb90nSJ2Gfy/hUOaDu6826e5w5UxPmbpCIc7KlBHNAZ5g4L4ZuHhc4hdfq4mzVBsQSnne+8Ze1g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -3537,15 +3552,15 @@ } }, "node_modules/@docusaurus/plugin-svgr": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.7.0.tgz", - "integrity": "sha512-HByXIZTbc4GV5VAUkZ2DXtXv1Qdlnpk3IpuImwSnEzCDBkUMYcec5282hPjn6skZqB25M1TYCmWS91UbhBGxQg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.0.tgz", + "integrity": "sha512-kEDyry+4OMz6BWLG/lEqrNsL/w818bywK70N1gytViw4m9iAmoxCUT7Ri9Dgs7xUdzCHJ3OujolEmD88Wy44OA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@svgr/core": "8.1.0", "@svgr/webpack": "^8.1.0", "tslib": "^2.6.0", @@ -3560,25 +3575,26 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz", - "integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.8.0.tgz", + "integrity": "sha512-qOu6tQDOWv+rpTlKu+eJATCJVGnABpRCPuqf7LbEaQ1mNY//N/P8cHQwkpAU+aweQfarcZ0XfwCqRHJfjeSV/g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/plugin-debug": "3.7.0", - "@docusaurus/plugin-google-analytics": "3.7.0", - "@docusaurus/plugin-google-gtag": "3.7.0", - "@docusaurus/plugin-google-tag-manager": "3.7.0", - "@docusaurus/plugin-sitemap": "3.7.0", - "@docusaurus/plugin-svgr": "3.7.0", - "@docusaurus/theme-classic": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-search-algolia": "3.7.0", - "@docusaurus/types": "3.7.0" + "@docusaurus/core": "3.8.0", + "@docusaurus/plugin-content-blog": "3.8.0", + "@docusaurus/plugin-content-docs": "3.8.0", + "@docusaurus/plugin-content-pages": "3.8.0", + "@docusaurus/plugin-css-cascade-layers": "3.8.0", + "@docusaurus/plugin-debug": "3.8.0", + "@docusaurus/plugin-google-analytics": "3.8.0", + "@docusaurus/plugin-google-gtag": "3.8.0", + "@docusaurus/plugin-google-tag-manager": "3.8.0", + "@docusaurus/plugin-sitemap": "3.8.0", + "@docusaurus/plugin-svgr": "3.8.0", + "@docusaurus/theme-classic": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/theme-search-algolia": "3.8.0", + "@docusaurus/types": "3.8.0" }, "engines": { "node": ">=18.0" @@ -3589,24 +3605,24 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.7.0.tgz", - "integrity": "sha512-MnLxG39WcvLCl4eUzHr0gNcpHQfWoGqzADCly54aqCofQX6UozOS9Th4RK3ARbM9m7zIRv3qbhggI53dQtx/hQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.8.0.tgz", + "integrity": "sha512-nQWFiD5ZjoT76OaELt2n33P3WVuuCz8Dt5KFRP2fCBo2r9JCLsp2GJjZpnaG24LZ5/arRjv4VqWKgpK0/YLt7g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/module-type-aliases": "3.8.0", + "@docusaurus/plugin-content-blog": "3.8.0", + "@docusaurus/plugin-content-docs": "3.8.0", + "@docusaurus/plugin-content-pages": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/theme-translations": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", @@ -3630,15 +3646,15 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.7.0.tgz", - "integrity": "sha512-8eJ5X0y+gWDsURZnBfH0WabdNm8XMCXHv8ENy/3Z/oQKwaB/EHt5lP9VsTDTf36lKEp0V6DjzjFyFIB+CetL0A==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.8.0.tgz", + "integrity": "sha512-YqV2vAWpXGLA+A3PMLrOMtqgTHJLDcT+1Caa6RF7N4/IWgrevy5diY8oIHFkXR/eybjcrFFjUPrHif8gSGs3Tw==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/module-type-aliases": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3658,19 +3674,19 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.7.0.tgz", - "integrity": "sha512-Al/j5OdzwRU1m3falm+sYy9AaB93S1XF1Lgk9Yc6amp80dNxJVplQdQTR4cYdzkGtuQqbzUA8+kaoYYO0RbK6g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.0.tgz", + "integrity": "sha512-GBZ5UOcPgiu6nUw153+0+PNWvFKweSnvKIL6Rp04H9olKb475jfKjAwCCtju5D2xs5qXHvCMvzWOg5o9f6DtuQ==", "license": "MIT", "dependencies": { - "@docsearch/react": "^3.8.1", - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docsearch/react": "^3.9.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/plugin-content-docs": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/theme-translations": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "algoliasearch": "^5.17.1", "algoliasearch-helper": "^3.22.6", "clsx": "^2.0.0", @@ -3689,9 +3705,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.7.0.tgz", - "integrity": "sha512-Ewq3bEraWDmienM6eaNK7fx+/lHMtGDHQyd1O+4+3EsDxxUmrzPkV7Ct3nBWTuE0MsoZr3yNwQVKjllzCMuU3g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.8.0.tgz", + "integrity": "sha512-1DTy/snHicgkCkryWq54fZvsAglTdjTx4qjOXgqnXJ+DIty1B+aPQrAVUu8LiM+6BiILfmNxYsxhKTj+BS3PZg==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", @@ -3702,16 +3718,16 @@ } }, "node_modules/@docusaurus/tsconfig": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz", - "integrity": "sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.8.0.tgz", + "integrity": "sha512-utLl48nNjSYBoq47RKukZ9fPLEX3nJWThzrujb0ndQQ1jc/gh4RhTRaAqItH9nImnsgGKmLMnyoMBpfGmoop+w==", "dev": true, "license": "MIT" }, "node_modules/@docusaurus/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz", - "integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.8.0.tgz", + "integrity": "sha512-RDEClpwNxZq02c+JlaKLWoS13qwWhjcNsi2wG1UpzmEnuti/z1Wx4SGpqbUqRPNSd8QWWePR8Cb7DvG0VN/TtA==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", @@ -3744,15 +3760,16 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.7.0.tgz", - "integrity": "sha512-e7zcB6TPnVzyUaHMJyLSArKa2AG3h9+4CfvKXKKWNx6hRs+p0a+u7HHTJBgo6KW2m+vqDnuIHK4X+bhmoghAFA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.8.0.tgz", + "integrity": "sha512-2wvtG28ALCN/A1WCSLxPASFBFzXCnP0YKCAFIPcvEb6imNu1wg7ni/Svcp71b3Z2FaOFFIv4Hq+j4gD7gA0yfQ==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-common": "3.8.0", "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", @@ -3762,9 +3779,9 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "micromatch": "^4.0.5", + "p-queue": "^6.6.2", "prompts": "^2.4.2", "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", "tslib": "^2.6.0", "url-loader": "^4.1.1", "utility-types": "^3.10.0", @@ -3775,12 +3792,12 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.7.0.tgz", - "integrity": "sha512-IZeyIfCfXy0Mevj6bWNg7DG7B8G+S6o6JVpddikZtWyxJguiQ7JYr0SIZ0qWd8pGNuMyVwriWmbWqMnK7Y5PwA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.8.0.tgz", + "integrity": "sha512-3TGF+wVTGgQ3pAc9+5jVchES4uXUAhAt9pwv7uws4mVOxL4alvU3ue/EZ+R4XuGk94pDy7CNXjRXpPjlfZXQfw==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.7.0", + "@docusaurus/types": "3.8.0", "tslib": "^2.6.0" }, "engines": { @@ -3788,14 +3805,14 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.7.0.tgz", - "integrity": "sha512-w8eiKk8mRdN+bNfeZqC4nyFoxNyI1/VExMKAzD9tqpJfLLbsa46Wfn5wcKH761g9WkKh36RtFV49iL9lh1DYBA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.8.0.tgz", + "integrity": "sha512-MrnEbkigr54HkdFeg8e4FKc4EF+E9dlVwsY3XQZsNkbv3MKZnbHQ5LsNJDIKDROFe8PBf5C4qCAg5TPBpsjrjg==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -4757,12 +4774,6 @@ "@types/node": "*" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/parse5": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", @@ -5437,15 +5448,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autocomplete.js": { "version": "0.37.1", "resolved": "https://registry.npmjs.org/autocomplete.js/-/autocomplete.js-0.37.1.tgz", @@ -7369,28 +7371,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "license": "MIT", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7463,38 +7443,6 @@ "node": ">= 4.0.0" } }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -8685,15 +8633,6 @@ "node": ">=0.10.0" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8834,134 +8773,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -9281,44 +9092,6 @@ "node": ">=10" } }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "license": "MIT", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -10644,13 +10417,10 @@ } }, "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, @@ -10994,15 +10764,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -11045,15 +10806,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -14650,6 +14402,15 @@ "node": ">=12.20" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -14695,6 +14456,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -14708,13 +14485,16 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/package-json": { @@ -15012,83 +14792,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", "funding": [ { "type": "opencollective", @@ -15105,7 +14812,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -16894,15 +16601,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -17094,132 +16792,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -17233,12 +16805,6 @@ "react": "^18.3.1" } }, - "node_modules/react-error-overlay": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", - "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", - "license": "MIT" - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -17270,15 +16836,15 @@ "license": "MIT" }, "node_modules/react-json-view-lite": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz", - "integrity": "sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz", + "integrity": "sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-loadable": { @@ -17433,12 +16999,6 @@ "node": ">=8.10.0" } }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -17514,18 +17074,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -18208,6 +17756,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, "node_modules/schema-utils": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", @@ -19472,12 +19026,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -19517,6 +19065,15 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinypool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", + "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -19686,6 +19243,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/docs/package.json b/docs/package.json index e13e85ecb33..2cc81f1b2c1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -16,8 +16,8 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "~3.7.0", - "@docusaurus/preset-classic": "~3.7.0", + "@docusaurus/core": "~3.8.0", + "@docusaurus/preset-classic": "~3.8.0", "@mdi/js": "^7.3.67", "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", @@ -35,7 +35,7 @@ "url": "^0.11.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "~3.7.0", + "@docusaurus/module-type-aliases": "~3.8.0", "@docusaurus/tsconfig": "^3.7.0", "@docusaurus/types": "^3.7.0", "prettier": "^3.2.4", diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx index 6a0981a5967..534d8e95d04 100644 --- a/docs/src/pages/cursed-knowledge.tsx +++ b/docs/src/pages/cursed-knowledge.tsx @@ -33,7 +33,7 @@ const items: Item[] = [ url: 'https://github.com/immich-app/immich/pull/17974', text: '#17974', }, - date: new Date(2025, 5, 5), + date: new Date(2025, 4, 5), }, { icon: mdiMicrosoftWindows, diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 343295c60d7..6d837d67ed4 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1709,17 +1709,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1733,7 +1733,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1749,16 +1749,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -1773,15 +1773,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1791,15 +1792,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1816,9 +1852,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -1830,14 +1866,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1883,16 +1921,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1907,13 +1945,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3834,9 +3872,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -6779,15 +6817,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 65a94122fa9..eedf70dc58e 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -428,6 +428,15 @@ describe('/albums', () => { order: AssetOrder.Desc, }); }); + + it('should not be able to share album with owner', async () => { + const { status, body } = await request(app) + .post('/albums') + .send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] }) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Cannot share album with owner')); + }); }); describe('PUT /albums/:id/assets', () => { diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 0148f2e1e93..bb6d17a2483 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -103,6 +103,7 @@ export const loginResponseDto = { accessToken: expect.any(String), name: 'Immich Admin', isAdmin: true, + isOnboarded: false, profileImagePath: '', shouldChangePassword: true, userEmail: 'admin@immich.cloud', diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index 74bee64e0a9..0fde9a6ec64 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -33,7 +33,9 @@ test.describe('Registration', () => { // onboarding await expect(page).toHaveURL('/auth/onboarding'); await page.getByRole('button', { name: 'Theme' }).click(); - await page.getByRole('button', { name: 'Privacy' }).click(); + await page.getByRole('button', { name: 'Language' }).click(); + await page.getByRole('button', { name: 'Server Privacy' }).click(); + await page.getByRole('button', { name: 'User Privacy' }).click(); await page.getByRole('button', { name: 'Storage Template' }).click(); await page.getByRole('button', { name: 'Done' }).click(); @@ -77,6 +79,13 @@ test.describe('Registration', () => { await page.getByLabel('Password').fill('new-password'); await page.getByRole('button', { name: 'Login' }).click(); + // onboarding + await expect(page).toHaveURL('/auth/onboarding'); + await page.getByRole('button', { name: 'Theme' }).click(); + await page.getByRole('button', { name: 'Language' }).click(); + await page.getByRole('button', { name: 'User Privacy' }).click(); + await page.getByRole('button', { name: 'Done' }).click(); + // success await expect(page).toHaveURL(/\/photos/); }); diff --git a/i18n/en.json b/i18n/en.json index 56e38cf8166..a25d62e94c8 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -26,7 +26,6 @@ "add_to_album": "Add to album", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", - "add_to_locked_folder": "Add to locked folder", "add_to_shared_album": "Add to shared album", "add_url": "Add URL", "added_to_archive": "Added to archive", @@ -44,9 +43,7 @@ "backup_database_enable_description": "Enable database dumps", "backup_keep_last_amount": "Amount of previous dumps to keep", "backup_settings": "Database Dump Settings", - "backup_settings_description": "Manage database dump settings. Note: These jobs are not monitored and you will not be notified of failure.", - "check_all": "Check All", - "cleanup": "Cleanup", + "backup_settings_description": "Manage database dump settings.", "cleared_jobs": "Cleared jobs for: {job}", "config_set_by_file": "Config is currently set by a config file", "confirm_delete_library": "Are you sure you want to delete {library} library?", @@ -62,14 +59,12 @@ "disable_login": "Disable login", "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search", "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", - "external_library_created_at": "External library (created on {date})", "external_library_management": "External Library Management", "face_detection": "Face detection", "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.", "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.", "failed_job_command": "Command {command} failed for job: {job}", "force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.", - "forcing_refresh_library_files": "Forcing refresh of all library files", "image_format": "Format", "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.", "image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in", @@ -210,8 +205,6 @@ "oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).", "oauth_timeout": "Request Timeout", "oauth_timeout_description": "Timeout for requests in milliseconds", - "offline_paths": "Offline Paths", - "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "password_enable_description": "Login with email and password", "password_settings": "Password Login", "password_settings_description": "Manage password login settings", @@ -221,9 +214,6 @@ "refreshing_all_libraries": "Refreshing all libraries", "registration": "Admin Registration", "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", - "repair_all": "Repair All", - "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", - "repaired_items": "Repaired {count, plural, one {# item} other {# items}}", "require_password_change_on_login": "Require user to change password on first login", "reset_settings_to_default": "Reset settings to default", "reset_settings_to_recent_saved": "Reset settings to the recent saved settings", @@ -264,7 +254,6 @@ "template_email_invite_album": "Invite Album Template", "template_email_preview": "Preview", "template_email_settings": "Email Templates", - "template_email_settings_description": "Manage custom email notification templates", "template_email_update_album": "Update Album Template", "template_email_welcome": "Welcome email template", "template_settings": "Notification Templates", @@ -273,7 +262,6 @@ "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.", "theme_settings": "Theme Settings", "theme_settings_description": "Manage customization of the Immich web interface", - "these_files_matched_by_checksum": "These files are matched by their checksums", "thumbnail_generation_job": "Generate Thumbnails", "thumbnail_generation_job_description": "Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person", "transcoding_acceleration_api": "Acceleration API", @@ -341,8 +329,6 @@ "trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them", "trash_settings": "Trash Settings", "trash_settings_description": "Manage trash settings", - "untracked_files": "Untracked Files", - "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "user_cleanup_job": "User cleanup", "user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Delete delay", @@ -401,10 +387,6 @@ "album_remove_user": "Remove user?", "album_remove_user_confirmation": "Are you sure you want to remove {user}?", "album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{count} items", - "album_thumbnail_card_shared": " · Shared", - "album_thumbnail_shared_by": "Shared by {user}", "album_updated": "Album updated", "album_updated_setting_description": "Receive an email notification when a shared album has new assets", "album_user_left": "Left {album}", @@ -495,6 +477,7 @@ "authorized_devices": "Authorized Devices", "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", "automatic_endpoint_switching_title": "Automatic URL switching", + "autoplay_slideshow": "Autoplay slideshow", "back": "Back", "back_close_deselect": "Back, close, or deselect", "background_location_permission": "Background location permission", @@ -576,21 +559,17 @@ "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.", "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.", "buy": "Purchase Immich", - "cache_settings_album_thumbnails": "Library page thumbnails ({count} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", "cache_settings_duplicated_assets_clear_button": "CLEAR", "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", "cache_settings_duplicated_assets_title": "Duplicated Assets ({count})", - "cache_settings_image_cache_size": "Image cache size ({count} assets)", "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{count} assets ({size})", "cache_settings_statistics_full": "Full images", "cache_settings_statistics_shared": "Shared album thumbnails", "cache_settings_statistics_thumbnail": "Thumbnails", "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({count} assets)", "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", @@ -604,6 +583,7 @@ "cannot_undo_this_action": "You cannot undo this action!", "cannot_update_the_description": "Cannot update the description", "cast": "Cast", + "cast_description": "Configure available cast destinations", "change_date": "Change date", "change_description": "Change description", "change_display_order": "Change display order", @@ -621,7 +601,6 @@ "change_pin_code": "Change PIN code", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", - "check_all": "Check All", "check_corrupt_asset_backup": "Check for corrupt asset backups", "check_corrupt_asset_backup_button": "Perform check", "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", @@ -667,7 +646,6 @@ "contain": "Contain", "context": "Context", "continue": "Continue", - "control_bottom_app_bar_album_info_shared": "{count} items · Shared", "control_bottom_app_bar_create_new_album": "Create new album", "control_bottom_app_bar_delete_from_immich": "Delete from Immich", "control_bottom_app_bar_delete_from_local": "Delete from device", @@ -778,7 +756,6 @@ "download_enqueue": "Download enqueued", "download_error": "Download Error", "download_failed": "Download failed", - "download_filename": "file: {filename}", "download_finished": "Download finished", "download_include_embedded_motion_videos": "Embedded videos", "download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file", @@ -854,7 +831,6 @@ "cant_get_number_of_comments": "Can't get number of comments", "cant_search_people": "Can't search people", "cant_search_places": "Can't search places", - "cleared_jobs": "Cleared jobs for: {job}", "error_adding_assets_to_album": "Error adding assets to album", "error_adding_users_to_album": "Error adding users to album", "error_deleting_shared_user": "Error deleting shared user", @@ -863,7 +839,6 @@ "error_removing_assets_from_album": "Error removing assets from album, check console for more details", "error_selecting_all_assets": "Error selecting all assets", "exclusion_pattern_already_exists": "This exclusion pattern already exists.", - "failed_job_command": "Command {command} failed for job: {job}", "failed_to_create_album": "Failed to create album", "failed_to_create_shared_link": "Failed to create shared link", "failed_to_edit_shared_link": "Failed to edit shared link", @@ -882,7 +857,6 @@ "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", "profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.", "quota_higher_than_disk_size": "You set a quota higher than the disk size", - "repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}", "unable_to_add_album_users": "Unable to add users to album", "unable_to_add_assets_to_shared_link": "Unable to add assets to shared link", "unable_to_add_comment": "Unable to add comment", @@ -901,7 +875,6 @@ "unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}", "unable_to_complete_oauth_login": "Unable to complete OAuth login", "unable_to_connect": "Unable to connect", - "unable_to_connect_to_server": "Unable to connect to server", "unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https", "unable_to_create_admin_account": "Unable to create admin account", "unable_to_create_api_key": "Unable to create a new API Key", @@ -925,14 +898,9 @@ "unable_to_hide_person": "Unable to hide person", "unable_to_link_motion_video": "Unable to link motion video", "unable_to_link_oauth_account": "Unable to link OAuth account", - "unable_to_load_album": "Unable to load album", - "unable_to_load_asset_activity": "Unable to load asset activity", - "unable_to_load_items": "Unable to load items", - "unable_to_load_liked_status": "Unable to load liked status", "unable_to_log_out_all_devices": "Unable to log out all devices", "unable_to_log_out_device": "Unable to log out device", "unable_to_login_with_oauth": "Unable to login with OAuth", - "unable_to_move_to_locked_folder": "Unable to move to locked folder", "unable_to_play_video": "Unable to play video", "unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}", "unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person", @@ -940,11 +908,9 @@ "unable_to_remove_album_users": "Unable to remove users from album", "unable_to_remove_api_key": "Unable to remove API Key", "unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link", - "unable_to_remove_deleted_assets": "Unable to remove offline files", "unable_to_remove_library": "Unable to remove library", "unable_to_remove_partner": "Unable to remove partner", "unable_to_remove_reaction": "Unable to remove reaction", - "unable_to_repair_items": "Unable to repair items", "unable_to_reset_password": "Unable to reset password", "unable_to_reset_pin_code": "Unable to reset PIN code", "unable_to_resolve_duplicate": "Unable to resolve duplicate", @@ -1027,6 +993,8 @@ "folders": "Folders", "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "forward": "Forward", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "This feature loads external resources from Google in order to work.", "general": "General", "get_help": "Get Help", "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", @@ -1115,6 +1083,12 @@ "invalid_date_format": "Invalid date format", "invite_people": "Invite People", "invite_to_album": "Invite to album", + "ios_debug_info_fetch_ran_at": "Fetch ran {dateTime}", + "ios_debug_info_last_sync_at": "Last sync {dateTime}", + "ios_debug_info_no_processes_queued": "No background processes queued", + "ios_debug_info_no_sync_yet": "No background sync job has run yet", + "ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}", + "ios_debug_info_processing_ran_at": "Processing ran {dateTime}", "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Jobs", "keep": "Keep", @@ -1123,6 +1097,9 @@ "kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Keyboard shortcuts", "language": "Language", + "language_no_results_subtitle": "Try adjusting your search term", + "language_no_results_title": "No languages found", + "language_search_hint": "Search languages...", "language_setting_description": "Select your preferred language", "last_seen": "Last seen", "latest_version": "Latest Version", @@ -1158,7 +1135,7 @@ "location_picker_longitude_error": "Enter a valid longitude", "location_picker_longitude_hint": "Enter your longitude here", "lock": "Lock", - "locked_folder": "Locked folder", + "locked_folder": "Locked Folder", "log_out": "Log out", "log_out_all_devices": "Log Out All Devices", "logged_out_all_devices": "Logged out all devices", @@ -1316,15 +1293,15 @@ "oauth": "OAuth", "official_immich_resources": "Official Immich Resources", "offline": "Offline", - "offline_paths": "Offline paths", - "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "ok": "Ok", "oldest_first": "Oldest first", "on_this_device": "On this device", "onboarding": "Onboarding", - "onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in the administration settings.", + "onboarding_locale_description": "Select your preferred language. You can change this later in your settings.", + "onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in settings.", + "onboarding_server_welcome_description": "Let's get your instance set up with some common settings.", "onboarding_theme_description": "Choose a color theme for your instance. You can change this later in your settings.", - "onboarding_welcome_description": "Let's get your instance set up with some common settings.", + "onboarding_user_welcome_description": "Let's get you started!", "onboarding_welcome_user": "Welcome, {user}", "online": "Online", "only_favorites": "Only favorites", @@ -1636,6 +1613,7 @@ "server_info_box_server_url": "Server URL", "server_offline": "Server Offline", "server_online": "Server Online", + "server_privacy": "Server Privacy", "server_stats": "Server Stats", "server_version": "Server Version", "set": "Set", @@ -1653,7 +1631,6 @@ "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {duration}", "setting_notifications_notify_hours": "{count} hours", "setting_notifications_notify_immediately": "immediately", @@ -1880,8 +1857,6 @@ "unselect_all_duplicates": "Unselect all duplicates", "unstack": "Un-stack", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", - "untracked_files": "Untracked files", - "untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "up_next": "Up next", "updated_at": "Updated", "updated_password": "Updated password", @@ -1909,6 +1884,7 @@ "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", "user_pin_code_settings": "PIN Code", "user_pin_code_settings_description": "Manage your PIN code", + "user_privacy": "User Privacy", "user_purchase_settings": "Purchase", "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", @@ -1924,11 +1900,6 @@ "version": "Version", "version_announcement_closing": "Your friend, Alex", "version_announcement_message": "Hi there! A new version of Immich is available. Please take some time to read the release notes to ensure your setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your Immich instance automatically.", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", "version_history": "Version History", "version_history_item": "Installed {version} on {date}", "video": "Video", diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 854f852e3cc..dc81c10dece 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -55,6 +55,7 @@ custom_lint: restrict: package:photo_manager allowed: # required / wanted + - 'lib/infrastructure/repositories/album_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 diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0ec511d9f12..7455ae99a25 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,103 +1,106 @@ plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" - id 'com.google.devtools.ksp' + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" + id 'com.google.devtools.ksp' } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { - localPropertiesFile.withInputStream { localProperties.load(it) } + localPropertiesFile.withInputStream { localProperties.load(it) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = '1.0' } def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { - keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } + keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } } android { - compileSdkVersion 35 + compileSdkVersion 35 - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - coreLibraryDesugaringEnabled true + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + coreLibraryDesugaringEnabled true + } + + kotlinOptions { + jvmTarget = '17' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "app.alextran.immich" + minSdkVersion 26 + targetSdkVersion 35 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + def keyAliasVal = System.getenv("ALIAS") + 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'] + } + } + + buildTypes { + debug { + applicationIdSuffix '.debug' + versionNameSuffix '-DEBUG' } - kotlinOptions { - jvmTarget = '17' + release { + signingConfig signingConfigs.release } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - applicationId "app.alextran.immich" - minSdkVersion 26 - targetSdkVersion 35 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - signingConfigs { - release { - def keyAliasVal = System.getenv("ALIAS") - 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'] - } - } - - buildTypes { - debug { - applicationIdSuffix '.debug' - versionNameSuffix '-DEBUG' - } - - release { - signingConfig signingConfigs.release - } - } - namespace 'app.alextran.immich' + } + namespace 'app.alextran.immich' } flutter { - source '../..' + source '../..' } 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 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' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - 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" - ksp "com.github.bumptech.glide:ksp:$glide_version" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + 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' } // This is uncommented in F-Droid build script 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 c1e5152d28b..f9c4ee2a1ff 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 @@ -1,6 +1,11 @@ package app.alextran.immich +import android.os.Build +import android.os.ext.SdkExtensions import androidx.annotation.NonNull +import app.alextran.immich.sync.NativeSyncApi +import app.alextran.immich.sync.NativeSyncApiImpl26 +import app.alextran.immich.sync.NativeSyncApiImpl30 import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine @@ -10,5 +15,13 @@ class MainActivity : FlutterFragmentActivity() { flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(HttpSSLOptionsPlugin()) // No need to set up method channel here as it's now handled in the plugin + + val nativeSyncApiImpl = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) { + NativeSyncApiImpl26(this) + } else { + NativeSyncApiImpl30(this) + } + NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl) } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt new file mode 100644 index 00000000000..f4dbda730b0 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -0,0 +1,393 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.sync + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object MessagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).containsKey(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PlatformAsset ( + val id: String, + val name: String, + val type: Long, + val createdAt: Long? = null, + val updatedAt: Long? = null, + val durationInSeconds: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformAsset { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val type = pigeonVar_list[2] as Long + val createdAt = pigeonVar_list[3] as Long? + val updatedAt = pigeonVar_list[4] as Long? + val durationInSeconds = pigeonVar_list[5] as Long + return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds) + } + } + fun toList(): List { + return listOf( + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PlatformAsset) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PlatformAlbum ( + val id: String, + val name: String, + val updatedAt: Long? = null, + val isCloud: Boolean, + val assetCount: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformAlbum { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val updatedAt = pigeonVar_list[2] as Long? + val isCloud = pigeonVar_list[3] as Boolean + val assetCount = pigeonVar_list[4] as Long + return PlatformAlbum(id, name, updatedAt, isCloud, assetCount) + } + } + fun toList(): List { + return listOf( + id, + name, + updatedAt, + isCloud, + assetCount, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PlatformAlbum) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SyncDelta ( + val hasChanges: Boolean, + val updates: List, + val deletes: List, + val assetAlbums: Map> +) + { + companion object { + fun fromList(pigeonVar_list: List): SyncDelta { + val hasChanges = pigeonVar_list[0] as Boolean + val updates = pigeonVar_list[1] as List + val deletes = pigeonVar_list[2] as List + val assetAlbums = pigeonVar_list[3] as Map> + return SyncDelta(hasChanges, updates, deletes, assetAlbums) + } + } + fun toList(): List { + return listOf( + hasChanges, + updates, + deletes, + assetAlbums, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is SyncDelta) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformAsset.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformAlbum.fromList(it) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + SyncDelta.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is PlatformAsset -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is PlatformAlbum -> { + stream.write(130) + writeValue(stream, value.toList()) + } + is SyncDelta -> { + stream.write(131) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface NativeSyncApi { + fun shouldFullSync(): Boolean + fun getMediaChanges(): SyncDelta + fun checkpointSync() + fun clearSyncCheckpoint() + fun getAssetIdsForAlbum(albumId: String): List + fun getAlbums(): List + fun getAssetsCountSince(albumId: String, timestamp: Long): Long + fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List + + companion object { + /** The codec used by NativeSyncApi. */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val taskQueue = binaryMessenger.makeBackgroundTaskQueue() + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.shouldFullSync()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getMediaChanges()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.checkpointSync() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.clearSyncCheckpoint() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val wrapped: List = try { + listOf(api.getAssetIdsForAlbum(albumIdArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getAlbums()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val timestampArg = args[1] as Long + val wrapped: List = try { + listOf(api.getAssetsCountSince(albumIdArg, timestampArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val updatedTimeCondArg = args[1] as Long? + val wrapped: List = try { + listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt new file mode 100644 index 00000000000..5deacc30db1 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt @@ -0,0 +1,24 @@ +package app.alextran.immich.sync + +import android.content.Context + + +class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { + override fun shouldFullSync(): Boolean { + return true + } + + // No-op for Android 10 and below + override fun checkpointSync() { + // Cannot throw exception as this is called from the Dart side + // during the full sync process as well + } + + override fun clearSyncCheckpoint() { + // No-op for Android 10 and below + } + + override fun getMediaChanges(): SyncDelta { + throw IllegalStateException("Method not supported on this Android version.") + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt new file mode 100644 index 00000000000..052032e143a --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt @@ -0,0 +1,89 @@ +package app.alextran.immich.sync + +import android.content.Context +import android.os.Build +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresExtension +import kotlinx.serialization.json.Json + +@RequiresApi(Build.VERSION_CODES.Q) +@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) +class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { + private val ctx: Context = context.applicationContext + private val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + + companion object { + const val SHARED_PREF_NAME = "Immich::MediaManager" + const val SHARED_PREF_MEDIA_STORE_VERSION_KEY = "MediaStore::getVersion" + const val SHARED_PREF_MEDIA_STORE_GEN_KEY = "MediaStore::getGeneration" + } + + private fun getSavedGenerationMap(): Map { + return prefs.getString(SHARED_PREF_MEDIA_STORE_GEN_KEY, null)?.let { + Json.decodeFromString>(it) + } ?: emptyMap() + } + + override fun clearSyncCheckpoint() { + prefs.edit().apply { + remove(SHARED_PREF_MEDIA_STORE_VERSION_KEY) + remove(SHARED_PREF_MEDIA_STORE_GEN_KEY) + apply() + } + } + + override fun shouldFullSync(): Boolean = + MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null) + + override fun checkpointSync() { + val genMap = MediaStore.getExternalVolumeNames(ctx) + .associateWith { MediaStore.getGeneration(ctx, it) } + + prefs.edit().apply { + putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx)) + putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap)) + apply() + } + } + + override fun getMediaChanges(): SyncDelta { + val genMap = getSavedGenerationMap() + val currentVolumes = MediaStore.getExternalVolumeNames(ctx) + val changed = mutableListOf() + val deleted = mutableListOf() + val assetAlbums = mutableMapOf>() + var hasChanges = genMap.keys != currentVolumes + + for (volume in currentVolumes) { + val currentGen = MediaStore.getGeneration(ctx, volume) + val storedGen = genMap[volume] ?: 0 + if (currentGen <= storedGen) { + continue + } + + hasChanges = true + + val selection = + "$MEDIA_SELECTION AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)" + val selectionArgs = arrayOf( + *MEDIA_SELECTION_ARGS, + storedGen.toString(), + storedGen.toString() + ) + + getAssets(getCursor(volume, selection, selectionArgs)).forEach { + when (it) { + is AssetResult.ValidAsset -> { + changed.add(it.asset) + assetAlbums[it.asset.id] = listOf(it.albumId) + } + + is AssetResult.InvalidAsset -> deleted.add(it.assetId) + } + } + } + // Unmounted volumes are handled in dart when the album is removed + return SyncDelta(hasChanges, changed, deleted, assetAlbums) + } +} 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 new file mode 100644 index 00000000000..23228553077 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -0,0 +1,177 @@ +package app.alextran.immich.sync + +import android.annotation.SuppressLint +import android.content.Context +import android.database.Cursor +import android.provider.MediaStore +import java.io.File + +sealed class AssetResult { + data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult() + data class InvalidAsset(val assetId: String) : AssetResult() +} + +@SuppressLint("InlinedApi") +open class NativeSyncApiImplBase(context: Context) { + private val ctx: Context = context.applicationContext + + companion object { + const val MEDIA_SELECTION = + "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" + val MEDIA_SELECTION_ARGS = arrayOf( + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString() + ) + const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)" + val ASSET_PROJECTION = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.DATE_TAKEN, + MediaStore.MediaColumns.DATE_ADDED, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.Files.FileColumns.MEDIA_TYPE, + MediaStore.MediaColumns.BUCKET_ID, + MediaStore.MediaColumns.DURATION + ) + } + + protected fun getCursor( + volume: String, + selection: String, + selectionArgs: Array, + projection: Array = ASSET_PROJECTION, + sortOrder: String? = null + ): Cursor? = ctx.contentResolver.query( + MediaStore.Files.getContentUri(volume), + projection, + selection, + selectionArgs, + sortOrder, + ) + + protected fun getAssets(cursor: Cursor?): Sequence { + return sequence { + cursor?.use { c -> + val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) + val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) + val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN) + val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) + val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) + val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID) + val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) + + while (c.moveToNext()) { + val id = c.getLong(idColumn).toString() + + val path = c.getString(dataColumn) + if (path.isNullOrBlank() || !File(path).exists()) { + yield(AssetResult.InvalidAsset(id)) + continue + } + + val mediaType = c.getInt(mediaTypeColumn) + val name = c.getString(nameColumn) + // Date taken is milliseconds since epoch, Date added is seconds since epoch + val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000)) + ?: c.getLong(dateAddedColumn) + // Date modified is seconds since epoch + val modifiedAt = c.getLong(dateModifiedColumn) + // Duration is milliseconds + val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 + else c.getLong(durationColumn) / 1000 + val bucketId = c.getString(bucketIdColumn) + + val asset = PlatformAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration) + yield(AssetResult.ValidAsset(asset, bucketId)) + } + } + } + } + + fun getAlbums(): List { + val albums = mutableListOf() + val albumsCount = mutableMapOf() + + val projection = arrayOf( + MediaStore.Files.FileColumns.BUCKET_ID, + MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME, + MediaStore.Files.FileColumns.DATE_MODIFIED, + ) + val selection = + "(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION" + + getCursor( + MediaStore.VOLUME_EXTERNAL, + selection, + MEDIA_SELECTION_ARGS, + projection, + "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC" + )?.use { cursor -> + val bucketIdColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID) + val bucketNameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME) + val dateModified = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED) + + while (cursor.moveToNext()) { + val id = cursor.getString(bucketIdColumn) + + val count = albumsCount.getOrDefault(id, 0) + if (count != 0) { + albumsCount[id] = count + 1 + continue + } + + val name = cursor.getString(bucketNameColumn) + val updatedAt = cursor.getLong(dateModified) + albums.add(PlatformAlbum(id, name, updatedAt, false, 0)) + albumsCount[id] = 1 + } + } + + return albums.map { it.copy(assetCount = albumsCount[it.id]?.toLong() ?: 0) } + .sortedBy { it.id } + } + + fun getAssetIdsForAlbum(albumId: String): List { + val projection = arrayOf(MediaStore.MediaColumns._ID) + + return getCursor( + MediaStore.VOLUME_EXTERNAL, + "$BUCKET_SELECTION AND $MEDIA_SELECTION", + arrayOf(albumId, *MEDIA_SELECTION_ARGS), + projection + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + generateSequence { + if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null + }.toList() + } ?: emptyList() + } + + fun getAssetsCountSince(albumId: String, timestamp: Long): Long = + getCursor( + MediaStore.VOLUME_EXTERNAL, + "$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION", + arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS), + )?.use { cursor -> cursor.count.toLong() } ?: 0L + + + fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List { + var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION" + val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS) + + if (updatedTimeCond != null) { + selection += " AND (${MediaStore.Files.FileColumns.DATE_MODIFIED} > ? OR ${MediaStore.Files.FileColumns.DATE_ADDED} > ?)" + selectionArgs.addAll(listOf(updatedTimeCond.toString(), updatedTimeCond.toString())) + } + + return getAssets(getCursor(MediaStore.VOLUME_EXTERNAL, selection, selectionArgs.toTypedArray())) + .mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset } + .toList() + } +} diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 74f8904a109..29c3a7c0567 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -1,26 +1,27 @@ pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - repositories { - google() - mavenCentral() - gradlePluginPortal() - } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.7.2' apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false - id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version '8.7.2' apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false + id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false } include ":app" diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 1870ef477f9..5cdec3d9245 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4,3],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":7,"references":[6],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[6],"type":"index","data":{"on":6,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}}]} \ No newline at end of file diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index 6d4630f1fb2..263a43c22c0 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" analyzer_plugin: dependency: "direct main" description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.0" args: dependency: transitive description: @@ -106,34 +101,42 @@ packages: dependency: transitive description: name: custom_lint - sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" + sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_builder: dependency: "direct main" description: name: custom_lint_builder - sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" + sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.0" file: dependency: transitive description: @@ -154,10 +157,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" glob: dependency: "direct main" description: @@ -198,14 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -367,4 +362,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0 <4.0.0" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 4cfd8abe819..2890a4a5954 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -5,9 +5,9 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - analyzer: ^6.0.0 - analyzer_plugin: ^0.11.3 - custom_lint_builder: ^0.6.4 + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + custom_lint_builder: ^0.7.5 glob: ^2.1.2 dev_dependencies: diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 311f19857b0..3cbbf83f017 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -89,6 +89,16 @@ FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = Sync; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -175,6 +185,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, @@ -224,6 +235,9 @@ dependencies = ( FAC6F8992D287C890078CB2F /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + B2CF7F8C2DDE4EBB00744BF6 /* Sync */, + ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; @@ -270,7 +284,6 @@ }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -278,6 +291,7 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index fd62618205c..55d08adc6aa 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -22,6 +22,9 @@ import UIKit BackgroundServicePlugin.registerBackgroundProcessing() BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) + + let controller: FlutterViewController = window?.rootViewController as! FlutterViewController + NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl()) BackgroundServicePlugin.setPluginRegistrantCallback { registry in if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift new file mode 100644 index 00000000000..0d7a3026884 --- /dev/null +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -0,0 +1,446 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsMessages(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashMessages(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashMessages(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashMessages(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformAsset: Hashable { + var id: String + var name: String + var type: Int64 + var createdAt: Int64? = nil + var updatedAt: Int64? = nil + var durationInSeconds: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? { + let id = pigeonVar_list[0] as! String + let name = pigeonVar_list[1] as! String + let type = pigeonVar_list[2] as! Int64 + let createdAt: Int64? = nilOrValue(pigeonVar_list[3]) + let updatedAt: Int64? = nilOrValue(pigeonVar_list[4]) + let durationInSeconds = pigeonVar_list[5] as! Int64 + + return PlatformAsset( + id: id, + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds + ) + } + func toList() -> [Any?] { + return [ + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ] + } + static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformAlbum: Hashable { + var id: String + var name: String + var updatedAt: Int64? = nil + var isCloud: Bool + var assetCount: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? { + let id = pigeonVar_list[0] as! String + let name = pigeonVar_list[1] as! String + let updatedAt: Int64? = nilOrValue(pigeonVar_list[2]) + let isCloud = pigeonVar_list[3] as! Bool + let assetCount = pigeonVar_list[4] as! Int64 + + return PlatformAlbum( + id: id, + name: name, + updatedAt: updatedAt, + isCloud: isCloud, + assetCount: assetCount + ) + } + func toList() -> [Any?] { + return [ + id, + name, + updatedAt, + isCloud, + assetCount, + ] + } + static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct SyncDelta: Hashable { + var hasChanges: Bool + var updates: [PlatformAsset] + var deletes: [String] + var assetAlbums: [String: [String]] + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? { + let hasChanges = pigeonVar_list[0] as! Bool + let updates = pigeonVar_list[1] as! [PlatformAsset] + let deletes = pigeonVar_list[2] as! [String] + let assetAlbums = pigeonVar_list[3] as! [String: [String]] + + return SyncDelta( + hasChanges: hasChanges, + updates: updates, + deletes: deletes, + assetAlbums: assetAlbums + ) + } + func toList() -> [Any?] { + return [ + hasChanges, + updates, + deletes, + assetAlbums, + ] + } + static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return PlatformAsset.fromList(self.readValue() as! [Any?]) + case 130: + return PlatformAlbum.fromList(self.readValue() as! [Any?]) + case 131: + return SyncDelta.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? PlatformAsset { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? PlatformAlbum { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? SyncDelta { + super.writeByte(131) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagesPigeonCodecWriter(data: data) + } +} + +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol NativeSyncApi { + func shouldFullSync() throws -> Bool + func getMediaChanges() throws -> SyncDelta + func checkpointSync() throws + func clearSyncCheckpoint() throws + func getAssetIdsForAlbum(albumId: String) throws -> [String] + func getAlbums() throws -> [PlatformAlbum] + func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class NativeSyncApiSetup { + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } + /// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + #if os(iOS) + let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() + #else + let taskQueue: FlutterTaskQueue? = nil + #endif + let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + shouldFullSyncChannel.setMessageHandler { _, reply in + do { + let result = try api.shouldFullSync() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + shouldFullSyncChannel.setMessageHandler(nil) + } + let getMediaChangesChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getMediaChangesChannel.setMessageHandler { _, reply in + do { + let result = try api.getMediaChanges() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getMediaChangesChannel.setMessageHandler(nil) + } + let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + checkpointSyncChannel.setMessageHandler { _, reply in + do { + try api.checkpointSync() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + checkpointSyncChannel.setMessageHandler(nil) + } + let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + clearSyncCheckpointChannel.setMessageHandler { _, reply in + do { + try api.clearSyncCheckpoint() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + clearSyncCheckpointChannel.setMessageHandler(nil) + } + let getAssetIdsForAlbumChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetIdsForAlbumChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + do { + let result = try api.getAssetIdsForAlbum(albumId: albumIdArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetIdsForAlbumChannel.setMessageHandler(nil) + } + let getAlbumsChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAlbumsChannel.setMessageHandler { _, reply in + do { + let result = try api.getAlbums() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAlbumsChannel.setMessageHandler(nil) + } + let getAssetsCountSinceChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetsCountSinceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + let timestampArg = args[1] as! Int64 + do { + let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetsCountSinceChannel.setMessageHandler(nil) + } + let getAssetsForAlbumChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetsForAlbumChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + let updatedTimeCondArg: Int64? = nilOrValue(args[1]) + do { + let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetsForAlbumChannel.setMessageHandler(nil) + } + } +} diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift new file mode 100644 index 00000000000..5d2f08691d2 --- /dev/null +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -0,0 +1,246 @@ +import Photos + +struct AssetWrapper: Hashable, Equatable { + let asset: PlatformAsset + + init(with asset: PlatformAsset) { + self.asset = asset + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.asset.id) + } + + static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool { + return lhs.asset.id == rhs.asset.id + } +} + +extension PHAsset { + func toPlatformAsset() -> PlatformAsset { + return PlatformAsset( + id: localIdentifier, + name: title(), + type: Int64(mediaType.rawValue), + createdAt: creationDate.map { Int64($0.timeIntervalSince1970) }, + updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, + durationInSeconds: Int64(duration) + ) + } +} + +class NativeSyncApiImpl: NativeSyncApi { + private let defaults: UserDefaults + private let changeTokenKey = "immich:changeToken" + private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] + + init(with defaults: UserDefaults = .standard) { + self.defaults = defaults + } + + @available(iOS 16, *) + private func getChangeToken() -> PHPersistentChangeToken? { + guard let data = defaults.data(forKey: changeTokenKey) else { + return nil + } + return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data) + } + + @available(iOS 16, *) + private func saveChangeToken(token: PHPersistentChangeToken) -> Void { + guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { + return + } + defaults.set(data, forKey: changeTokenKey) + } + + func clearSyncCheckpoint() -> Void { + defaults.removeObject(forKey: changeTokenKey) + } + + func checkpointSync() { + guard #available(iOS 16, *) else { + return + } + saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken) + } + + func shouldFullSync() -> Bool { + guard #available(iOS 16, *), + PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized, + let storedToken = getChangeToken() else { + // When we do not have access to photo library, older iOS version or No token available, fallback to full sync + return true + } + + guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else { + // Cannot fetch persistent changes + return true + } + + return false + } + + func getAlbums() throws -> [PlatformAlbum] { + var albums: [PlatformAlbum] = [] + + albumTypes.forEach { type in + let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) + collections.enumerateObjects { (album, _, _) in + let options = PHFetchOptions() + options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)] + let assets = PHAsset.fetchAssets(in: album, options: options) + let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream + + var domainAlbum = PlatformAlbum( + id: album.localIdentifier, + name: album.localizedTitle!, + updatedAt: nil, + isCloud: isCloud, + assetCount: Int64(assets.count) + ) + + if let firstAsset = assets.firstObject { + domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) } + } + + albums.append(domainAlbum) + } + } + return albums.sorted { $0.id < $1.id } + } + + func getMediaChanges() throws -> SyncDelta { + guard #available(iOS 16, *) else { + throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil) + } + + guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { + throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil) + } + + guard let storedToken = getChangeToken() else { + // No token exists, definitely need a full sync + print("MediaManager::getMediaChanges: No token found") + throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil) + } + + let currentToken = PHPhotoLibrary.shared().currentChangeToken + if storedToken == currentToken { + return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:]) + } + + do { + let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) + + var updatedAssets: Set = [] + var deletedAssets: Set = [] + + for change in changes { + guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue } + + let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers) + deletedAssets.formUnion(details.deletedLocalIdentifiers) + + if (updated.isEmpty) { continue } + + let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil) + for i in 0..) -> [String: [String]] { + guard !assets.isEmpty else { + return [:] + } + + var albumAssets: [String: [String]] = [:] + + for type in albumTypes { + let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) + collections.enumerateObjects { (album, _, _) in + let options = PHFetchOptions() + options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id)) + let result = PHAsset.fetchAssets(in: album, options: options) + result.enumerateObjects { (asset, _, _) in + albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier) + } + } + } + return albumAssets + } + + func getAssetIdsForAlbum(albumId: String) throws -> [String] { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return [] + } + + var ids: [String] = [] + let assets = PHAsset.fetchAssets(in: album, options: nil) + assets.enumerateObjects { (asset, _, _) in + ids.append(asset.localIdentifier) + } + return ids + } + + func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return 0 + } + + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) + let options = PHFetchOptions() + options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) + let assets = PHAsset.fetchAssets(in: album, options: options) + return Int64(assets.count) + } + + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return [] + } + + let options = PHFetchOptions() + if(updatedTimeCond != nil) { + let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!)) + options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) + } + + let result = PHAsset.fetchAssets(in: album, options: options) + if(result.count == 0) { + return [] + } + + var assets: [PlatformAsset] = [] + result.enumerateObjects { (asset, _, _) in + assets.append(asset.toPlatformAsset()) + } + return assets + } +} diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 33683afd92f..8c95922a3ab 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -7,6 +7,7 @@ const int kLogTruncateLimit = 250; // Sync const int kSyncEventBatchSize = 5000; +const int kFetchLocalAssetsBatchSize = 40000; // Hash batch limits const int kBatchHashFileLimit = 128; diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index b4d4b63660c..658242ea3ad 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -5,6 +5,7 @@ const Map locales = { 'English (en)': Locale('en'), // Additional locales 'Arabic (ar)': Locale('ar'), + 'Bulgarian (bg)': Locale('bg'), 'Catalan (ca)': Locale('ca'), 'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'), @@ -31,6 +32,7 @@ const Map locales = { 'Mongolian (mn)': Locale('mn'), 'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'), 'Polish (pl)': Locale('pl'), + 'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'), 'Portuguese (pt)': Locale('pt'), 'Romanian (ro)': Locale('ro'), 'Russian (ru)': Locale('ru'), @@ -42,6 +44,8 @@ const Map locales = { 'Slovenian (sl)': Locale('sl'), 'Spanish (es)': Locale('es'), 'Swedish (sv)': Locale('sv'), + 'Tamil (ta)': Locale('ta'), + 'Telugu (te)': Locale('te'), 'Thai (th)': Locale('th'), 'Turkish (tr)': Locale('tr'), 'Ukrainian (uk)': Locale('uk'), diff --git a/mobile/lib/domain/interfaces/local_album.interface.dart b/mobile/lib/domain/interfaces/local_album.interface.dart new file mode 100644 index 00000000000..4cf46904938 --- /dev/null +++ b/mobile/lib/domain/interfaces/local_album.interface.dart @@ -0,0 +1,35 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; + +abstract interface class ILocalAlbumRepository implements IDatabaseRepository { + Future> getAll({SortLocalAlbumsBy? sortBy}); + + Future> getAssetsForAlbum(String albumId); + + Future> getAssetIdsForAlbum(String albumId); + + Future upsert( + LocalAlbum album, { + Iterable toUpsert = const [], + Iterable toDelete = const [], + }); + + Future update(LocalAlbum album); + Future updateAll(Iterable albums); + + Future delete(String albumId); + + Future processDelta({ + required List updates, + required List deletes, + required Map> assetAlbums, + }); + + Future syncAlbumDeletes( + String albumId, + Iterable assetIdsToKeep, + ); +} + +enum SortLocalAlbumsBy { id } diff --git a/mobile/lib/domain/models/asset/asset.model.dart b/mobile/lib/domain/models/asset/asset.model.dart new file mode 100644 index 00000000000..c170f7f8485 --- /dev/null +++ b/mobile/lib/domain/models/asset/asset.model.dart @@ -0,0 +1,61 @@ +part of 'base_asset.model.dart'; + +enum AssetVisibility { + timeline, + hidden, + archive, + locked, +} + +// Model for an asset stored in the server +class Asset extends BaseAsset { + final String id; + final String? localId; + final AssetVisibility visibility; + + const Asset({ + required this.id, + this.localId, + required super.name, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + this.visibility = AssetVisibility.timeline, + }); + + @override + String toString() { + return '''Asset { + id: $id, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + localId: ${localId ?? ""}, + isFavorite: $isFavorite, + visibility: $visibility, + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! Asset) return false; + if (identical(this, other)) return true; + return super == other && + id == other.id && + localId == other.localId && + visibility == other.visibility; + } + + @override + int get hashCode => + super.hashCode ^ id.hashCode ^ localId.hashCode ^ visibility.hashCode; +} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart new file mode 100644 index 00000000000..fb954376597 --- /dev/null +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -0,0 +1,76 @@ +part 'asset.model.dart'; +part 'local_asset.model.dart'; + +enum AssetType { + // do not change this order! + other, + image, + video, + audio, +} + +sealed class BaseAsset { + final String name; + final String? checksum; + final AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final bool isFavorite; + + const BaseAsset({ + required this.name, + required this.checksum, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + this.isFavorite = false, + }); + + @override + String toString() { + return '''BaseAsset { + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + isFavorite: $isFavorite, +}'''; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is BaseAsset) { + return name == other.name && + type == other.type && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + width == other.width && + height == other.height && + durationInSeconds == other.durationInSeconds && + isFavorite == other.isFavorite; + } + return false; + } + + @override + int get hashCode { + return name.hashCode ^ + type.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + width.hashCode ^ + height.hashCode ^ + durationInSeconds.hashCode ^ + isFavorite.hashCode; + } +} diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart new file mode 100644 index 00000000000..25e617d8edc --- /dev/null +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -0,0 +1,74 @@ +part of 'base_asset.model.dart'; + +class LocalAsset extends BaseAsset { + final String id; + final String? remoteId; + + const LocalAsset({ + required this.id, + this.remoteId, + required super.name, + super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + }); + + @override + String toString() { + return '''LocalAsset { + id: $id, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + remoteId: ${remoteId ?? ""} + isFavorite: $isFavorite, + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! LocalAsset) return false; + if (identical(this, other)) return true; + return super == other && id == other.id && remoteId == other.remoteId; + } + + @override + int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode; + + LocalAsset copyWith({ + String? id, + String? remoteId, + String? name, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + bool? isFavorite, + }) { + return LocalAsset( + id: id ?? this.id, + remoteId: remoteId ?? this.remoteId, + name: name ?? this.name, + 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, + ); + } +} diff --git a/mobile/lib/domain/models/local_album.model.dart b/mobile/lib/domain/models/local_album.model.dart new file mode 100644 index 00000000000..95c56627bbd --- /dev/null +++ b/mobile/lib/domain/models/local_album.model.dart @@ -0,0 +1,70 @@ +enum BackupSelection { + none, + selected, + excluded, +} + +class LocalAlbum { + final String id; + final String name; + final DateTime updatedAt; + + final int assetCount; + final BackupSelection backupSelection; + + const LocalAlbum({ + required this.id, + required this.name, + required this.updatedAt, + this.assetCount = 0, + this.backupSelection = BackupSelection.none, + }); + + LocalAlbum copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? assetCount, + BackupSelection? backupSelection, + }) { + return LocalAlbum( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + assetCount: assetCount ?? this.assetCount, + backupSelection: backupSelection ?? this.backupSelection, + ); + } + + @override + bool operator ==(Object other) { + if (other is! LocalAlbum) return false; + if (identical(this, other)) return true; + + return other.id == id && + other.name == name && + other.updatedAt == updatedAt && + other.assetCount == assetCount && + other.backupSelection == backupSelection; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + updatedAt.hashCode ^ + assetCount.hashCode ^ + backupSelection.hashCode; + } + + @override + String toString() { + return '''LocalAlbum: { +id: $id, +name: $name, +updatedAt: $updatedAt, +assetCount: $assetCount, +backupSelection: $backupSelection, +}'''; + } +} diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart new file mode 100644 index 00000000000..e07595b6dbd --- /dev/null +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -0,0 +1,379 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; +import 'package:immich_mobile/utils/diff.dart'; +import 'package:logging/logging.dart'; +import 'package:platform/platform.dart'; + +class LocalSyncService { + final ILocalAlbumRepository _localAlbumRepository; + final NativeSyncApi _nativeSyncApi; + final Platform _platform; + final StoreService _storeService; + final Logger _log = Logger("DeviceSyncService"); + + LocalSyncService({ + required ILocalAlbumRepository localAlbumRepository, + required NativeSyncApi nativeSyncApi, + required StoreService storeService, + Platform? platform, + }) : _localAlbumRepository = localAlbumRepository, + _nativeSyncApi = nativeSyncApi, + _storeService = storeService, + _platform = platform ?? const LocalPlatform(); + + bool get _ignoreIcloudAssets => + _storeService.get(StoreKey.ignoreIcloudAssets, false) == true; + + Future sync({bool full = false}) async { + final Stopwatch stopwatch = Stopwatch()..start(); + try { + if (full || await _nativeSyncApi.shouldFullSync()) { + _log.fine("Full sync request from ${full ? "user" : "native"}"); + DLog.log("Full sync request from ${full ? "user" : "native"}"); + return await fullSync(); + } + + final delta = await _nativeSyncApi.getMediaChanges(); + if (!delta.hasChanges) { + _log.fine("No media changes detected. Skipping sync"); + DLog.log("No media changes detected. Skipping sync"); + return; + } + + DLog.log("Delta updated: ${delta.updates.length}"); + DLog.log("Delta deleted: ${delta.deletes.length}"); + + final deviceAlbums = await _nativeSyncApi.getAlbums(); + await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); + await _localAlbumRepository.processDelta( + updates: delta.updates.toLocalAssets(), + deletes: delta.deletes, + assetAlbums: delta.assetAlbums, + ); + + final dbAlbums = await _localAlbumRepository.getAll(); + // On Android, we need to sync all albums since it is not possible to + // detect album deletions from the native side + if (_platform.isAndroid) { + for (final album in dbAlbums) { + final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id); + await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds); + } + } + + if (_platform.isIOS) { + // On iOS, we need to full sync albums that are marked as cloud as the delta sync + // does not include changes for cloud albums. If ignoreIcloudAssets is enabled, + // remove the albums from the local database from the previous sync + final cloudAlbums = + deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); + for (final album in cloudAlbums) { + final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id); + if (dbAlbum == null) { + _log.warning( + "Cloud album ${album.name} not found in local database. Skipping sync.", + ); + continue; + } + if (_ignoreIcloudAssets) { + await removeAlbum(dbAlbum); + } else { + await updateAlbum(dbAlbum, album); + } + } + } + + await _nativeSyncApi.checkpointSync(); + } catch (e, s) { + _log.severe("Error performing device sync", e, s); + } finally { + stopwatch.stop(); + _log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); + DLog.log("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); + } + } + + Future fullSync() async { + try { + final Stopwatch stopwatch = Stopwatch()..start(); + + List deviceAlbums = + List.of(await _nativeSyncApi.getAlbums()); + if (_platform.isIOS && _ignoreIcloudAssets) { + deviceAlbums.removeWhere((album) => album.isCloud); + } + + final dbAlbums = + await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id); + + await diffSortedLists( + dbAlbums, + deviceAlbums.toLocalAlbums(), + compare: (a, b) => a.id.compareTo(b.id), + both: updateAlbum, + onlyFirst: removeAlbum, + onlySecond: addAlbum, + ); + + await _nativeSyncApi.checkpointSync(); + stopwatch.stop(); + _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); + DLog.log("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); + } catch (e, s) { + _log.severe("Error performing full device sync", e, s); + } + } + + Future addAlbum(LocalAlbum album) async { + try { + _log.fine("Adding device album ${album.name}"); + + final assets = album.assetCount > 0 + ? await _nativeSyncApi.getAssetsForAlbum(album.id) + : []; + + await _localAlbumRepository.upsert( + album, + toUpsert: assets.toLocalAssets(), + ); + _log.fine("Successfully added device album ${album.name}"); + } catch (e, s) { + _log.warning("Error while adding device album", e, s); + } + } + + Future removeAlbum(LocalAlbum a) async { + _log.fine("Removing device album ${a.name}"); + try { + // Asset deletion is handled in the repository + await _localAlbumRepository.delete(a.id); + } catch (e, s) { + _log.warning("Error while removing device album", e, s); + } + } + + // The deviceAlbum is ignored since we are going to refresh it anyways + FutureOr updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { + try { + _log.fine("Syncing device album ${dbAlbum.name}"); + + if (_albumsEqual(deviceAlbum, dbAlbum)) { + _log.fine( + "Device album ${dbAlbum.name} has not changed. Skipping sync.", + ); + return false; + } + + _log.fine("Device album ${dbAlbum.name} has changed. Syncing..."); + + // Faster path - only new assets added + if (await checkAddition(dbAlbum, deviceAlbum)) { + _log.fine("Fast synced device album ${dbAlbum.name}"); + DLog.log("Fast synced device album ${dbAlbum.name}"); + return true; + } + + // Slower path - full sync + return await fullDiff(dbAlbum, deviceAlbum); + } catch (e, s) { + _log.warning("Error while diff device album", e, s); + } + return true; + } + + @visibleForTesting + // The [deviceAlbum] is expected to be refreshed before calling this method + // with modified time and asset count + Future checkAddition( + LocalAlbum dbAlbum, + LocalAlbum deviceAlbum, + ) async { + try { + _log.fine("Fast syncing device album ${dbAlbum.name}"); + // Assets has been modified + if (deviceAlbum.assetCount <= dbAlbum.assetCount) { + _log.fine("Local album has modifications. Proceeding to full sync"); + return false; + } + + final updatedTime = + (dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1; + final newAssetsCount = + await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime); + + // Early return if no new assets were found + if (newAssetsCount == 0) { + _log.fine( + "No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}", + ); + return false; + } + + // Check whether there is only addition or if there has been deletions + if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssetsCount) { + _log.fine("Local album has modifications. Proceeding to full sync"); + return false; + } + + final newAssets = await _nativeSyncApi.getAssetsForAlbum( + deviceAlbum.id, + updatedTimeCond: updatedTime, + ); + + await _localAlbumRepository.upsert( + deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), + toUpsert: newAssets.toLocalAssets(), + ); + + return true; + } catch (e, s) { + _log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s); + } + return false; + } + + @visibleForTesting + // The [deviceAlbum] is expected to be refreshed before calling this method + // with modified time and asset count + Future fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { + try { + final assetsInDevice = deviceAlbum.assetCount > 0 + ? await _nativeSyncApi + .getAssetsForAlbum(deviceAlbum.id) + .then((a) => a.toLocalAssets()) + : []; + final assetsInDb = dbAlbum.assetCount > 0 + ? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id) + : []; + + if (deviceAlbum.assetCount == 0) { + _log.fine( + "Device album ${deviceAlbum.name} is empty. Removing assets from DB.", + ); + await _localAlbumRepository.upsert( + deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), + toDelete: assetsInDb.map((a) => a.id), + ); + return true; + } + + final updatedDeviceAlbum = deviceAlbum.copyWith( + backupSelection: dbAlbum.backupSelection, + ); + + if (dbAlbum.assetCount == 0) { + _log.fine( + "Device album ${deviceAlbum.name} is empty. Adding assets to DB.", + ); + await _localAlbumRepository.upsert( + updatedDeviceAlbum, + toUpsert: assetsInDevice, + ); + return true; + } + + assert(assetsInDb.isSortedBy((a) => a.id)); + assetsInDevice.sort((a, b) => a.id.compareTo(b.id)); + + final assetsToUpsert = []; + final assetsToDelete = []; + + diffSortedListsSync( + assetsInDb, + assetsInDevice, + compare: (a, b) => a.id.compareTo(b.id), + both: (dbAsset, deviceAsset) { + // Custom comparison to check if the asset has been modified without + // comparing the checksum + if (!_assetsEqual(dbAsset, deviceAsset)) { + assetsToUpsert.add(deviceAsset); + return true; + } + return false; + }, + onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.id), + onlySecond: (deviceAsset) => assetsToUpsert.add(deviceAsset), + ); + + _log.fine( + "Syncing ${deviceAlbum.name}. ${assetsToUpsert.length} assets to add/update and ${assetsToDelete.length} assets to delete", + ); + + if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) { + _log.fine( + "No asset changes detected in album ${deviceAlbum.name}. Updating metadata.", + ); + _localAlbumRepository.upsert(updatedDeviceAlbum); + return true; + } + + await _localAlbumRepository.upsert( + updatedDeviceAlbum, + toUpsert: assetsToUpsert, + toDelete: assetsToDelete, + ); + + return true; + } catch (e, s) { + _log.warning("Error on full syncing local album: ${dbAlbum.name}", e, s); + } + return true; + } + + bool _assetsEqual(LocalAsset a, LocalAsset b) { + return a.updatedAt.isAtSameMomentAs(b.updatedAt) && + a.createdAt.isAtSameMomentAs(b.createdAt) && + a.width == b.width && + a.height == b.height && + a.durationInSeconds == b.durationInSeconds; + } + + bool _albumsEqual(LocalAlbum a, LocalAlbum b) { + return a.name == b.name && + a.assetCount == b.assetCount && + a.updatedAt.isAtSameMomentAs(b.updatedAt); + } +} + +extension on Iterable { + List toLocalAlbums() { + return map( + (e) => LocalAlbum( + id: e.id, + name: e.name, + updatedAt: e.updatedAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + assetCount: e.assetCount, + ), + ).toList(); + } +} + +extension on Iterable { + List toLocalAssets() { + return map( + (e) => LocalAsset( + id: e.id, + name: e.name, + type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, + createdAt: e.createdAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000), + updatedAt: e.updatedAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + durationInSeconds: e.durationInSeconds, + ), + ).toList(); + } +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index ac63734b07d..00f97825b23 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -63,7 +63,6 @@ class SyncStreamService { Iterable data, ) async { _logger.fine("Processing sync data for $type of length ${data.length}"); - // ignore: prefer-switch-expression switch (type) { case SyncEntityType.userV1: return _syncStreamRepository.updateUsersV1(data.cast()); diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index f63dc81ba99..6a694ee44ac 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -1,13 +1,12 @@ -// ignore_for_file: avoid-passing-async-when-sync-expected - import 'dart:async'; -import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; import 'package:immich_mobile/utils/isolate.dart'; import 'package:worker_manager/worker_manager.dart'; class BackgroundSyncManager { Cancelable? _syncTask; + Cancelable? _deviceAlbumSyncTask; BackgroundSyncManager(); @@ -23,7 +22,30 @@ class BackgroundSyncManager { return Future.wait(futures); } - Future sync() { + // No need to cancel the task, as it can also be run when the user logs out + Future syncLocal({bool full = false}) { + if (_deviceAlbumSyncTask != null) { + return _deviceAlbumSyncTask!.future; + } + + // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being + // captured by the closure passed to [runInIsolateGentle]. + _deviceAlbumSyncTask = full + ? runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: true), + ) + : runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: false), + ); + + return _deviceAlbumSyncTask!.whenComplete(() { + _deviceAlbumSyncTask = null; + }); + } + + Future syncRemote() { if (_syncTask != null) { return _syncTask!.future; } @@ -31,9 +53,8 @@ class BackgroundSyncManager { _syncTask = runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).sync(), ); - _syncTask!.whenComplete(() { + return _syncTask!.whenComplete(() { _syncTask = null; }); - return _syncTask!.future; } } diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart index 73c8c2d34c3..67411013eed 100644 --- a/mobile/lib/extensions/string_extensions.dart +++ b/mobile/lib/extensions/string_extensions.dart @@ -1,7 +1,3 @@ -import 'dart:typed_data'; - -import 'package:uuid/parsing.dart'; - extension StringExtension on String { String capitalize() { return split(" ") @@ -33,8 +29,3 @@ extension DurationExtension on String { return int.parse(this); } } - -extension UUIDExtension on String { - Uint8List toUuidByte({bool shouldValidate = false}) => - UuidParsing.parseAsByteList(this, validate: shouldValidate); -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 5a93bc97681..11730b77615 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -1,4 +1,7 @@ +import 'package:drift/drift.dart' hide Query; import 'package:immich_mobile/domain/models/exif.model.dart' as domain; +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'; @@ -90,3 +93,53 @@ class ExifInfo { exposureSeconds: exposureSeconds, ); } + +class RemoteExifEntity extends Table with DriftDefaultsMixin { + const RemoteExifEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get city => text().nullable()(); + + TextColumn get state => text().nullable()(); + + TextColumn get country => text().nullable()(); + + DateTimeColumn get dateTimeOriginal => dateTime().nullable()(); + + TextColumn get description => text().nullable()(); + + IntColumn get height => integer().nullable()(); + + IntColumn get width => integer().nullable()(); + + TextColumn get exposureTime => text().nullable()(); + + IntColumn get fNumber => integer().nullable()(); + + IntColumn get fileSize => integer().nullable()(); + + IntColumn get focalLength => integer().nullable()(); + + IntColumn get latitude => integer().nullable()(); + + IntColumn get longitude => integer().nullable()(); + + IntColumn get iso => integer().nullable()(); + + TextColumn get make => text().nullable()(); + + TextColumn get model => text().nullable()(); + + TextColumn get orientation => text().nullable()(); + + TextColumn get timeZone => text().nullable()(); + + IntColumn get rating => integer().nullable()(); + + TextColumn get projectionType => text().nullable()(); + + @override + Set get primaryKey => {assetId}; +} diff --git a/mobile/lib/infrastructure/entities/exif.entity.drift.dart b/mobile/lib/infrastructure/entities/exif.entity.drift.dart new file mode 100644 index 00000000000..10025d9cb87 --- /dev/null +++ b/mobile/lib/infrastructure/entities/exif.entity.drift.dart @@ -0,0 +1,1484 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; + +typedef $$RemoteExifEntityTableCreateCompanionBuilder + = i1.RemoteExifEntityCompanion Function({ + required String assetId, + i0.Value city, + i0.Value state, + i0.Value country, + i0.Value dateTimeOriginal, + i0.Value description, + i0.Value height, + i0.Value width, + i0.Value exposureTime, + i0.Value fNumber, + i0.Value fileSize, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, + i0.Value iso, + i0.Value make, + i0.Value model, + i0.Value orientation, + i0.Value timeZone, + i0.Value rating, + i0.Value projectionType, +}); +typedef $$RemoteExifEntityTableUpdateCompanionBuilder + = i1.RemoteExifEntityCompanion Function({ + i0.Value assetId, + i0.Value city, + i0.Value state, + i0.Value country, + i0.Value dateTimeOriginal, + i0.Value description, + i0.Value height, + i0.Value width, + i0.Value exposureTime, + i0.Value fNumber, + i0.Value fileSize, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, + i0.Value iso, + i0.Value make, + i0.Value model, + i0.Value orientation, + i0.Value timeZone, + i0.Value rating, + i0.Value projectionType, +}); + +final class $$RemoteExifEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$RemoteExifEntityTable, i1.RemoteExifEntityData> { + $$RemoteExifEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('remote_exif_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteExifEntityTableFilterComposer + extends i0.Composer { + $$RemoteExifEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get city => $composableBuilder( + column: $table.city, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get state => $composableBuilder( + column: $table.state, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get country => $composableBuilder( + column: $table.country, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get exposureTime => $composableBuilder( + column: $table.exposureTime, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get fNumber => $composableBuilder( + column: $table.fNumber, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get focalLength => $composableBuilder( + column: $table.focalLength, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get longitude => $composableBuilder( + column: $table.longitude, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get iso => $composableBuilder( + column: $table.iso, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get make => $composableBuilder( + column: $table.make, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get model => $composableBuilder( + column: $table.model, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get timeZone => $composableBuilder( + column: $table.timeZone, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get rating => $composableBuilder( + column: $table.rating, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get projectionType => $composableBuilder( + column: $table.projectionType, + builder: (column) => i0.ColumnFilters(column)); + + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableOrderingComposer + extends i0.Composer { + $$RemoteExifEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get city => $composableBuilder( + column: $table.city, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get state => $composableBuilder( + column: $table.state, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get country => $composableBuilder( + column: $table.country, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get exposureTime => $composableBuilder( + column: $table.exposureTime, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get fNumber => $composableBuilder( + column: $table.fNumber, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get focalLength => $composableBuilder( + column: $table.focalLength, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get longitude => $composableBuilder( + column: $table.longitude, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get iso => $composableBuilder( + column: $table.iso, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get make => $composableBuilder( + column: $table.make, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get model => $composableBuilder( + column: $table.model, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get timeZone => $composableBuilder( + column: $table.timeZone, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get rating => $composableBuilder( + column: $table.rating, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get projectionType => $composableBuilder( + column: $table.projectionType, + builder: (column) => i0.ColumnOrderings(column)); + + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteExifEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get city => + $composableBuilder(column: $table.city, builder: (column) => column); + + i0.GeneratedColumn get state => + $composableBuilder(column: $table.state, builder: (column) => column); + + i0.GeneratedColumn get country => + $composableBuilder(column: $table.country, builder: (column) => column); + + i0.GeneratedColumn get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, builder: (column) => column); + + i0.GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + i0.GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + i0.GeneratedColumn get width => + $composableBuilder(column: $table.width, builder: (column) => column); + + i0.GeneratedColumn get exposureTime => $composableBuilder( + column: $table.exposureTime, builder: (column) => column); + + i0.GeneratedColumn get fNumber => + $composableBuilder(column: $table.fNumber, builder: (column) => column); + + i0.GeneratedColumn get fileSize => + $composableBuilder(column: $table.fileSize, builder: (column) => column); + + i0.GeneratedColumn get focalLength => $composableBuilder( + column: $table.focalLength, builder: (column) => column); + + i0.GeneratedColumn get latitude => + $composableBuilder(column: $table.latitude, builder: (column) => column); + + i0.GeneratedColumn get longitude => + $composableBuilder(column: $table.longitude, builder: (column) => column); + + i0.GeneratedColumn get iso => + $composableBuilder(column: $table.iso, builder: (column) => column); + + i0.GeneratedColumn get make => + $composableBuilder(column: $table.make, builder: (column) => column); + + i0.GeneratedColumn get model => + $composableBuilder(column: $table.model, builder: (column) => column); + + i0.GeneratedColumn get orientation => $composableBuilder( + column: $table.orientation, builder: (column) => column); + + i0.GeneratedColumn get timeZone => + $composableBuilder(column: $table.timeZone, builder: (column) => column); + + i0.GeneratedColumn get rating => + $composableBuilder(column: $table.rating, builder: (column) => column); + + i0.GeneratedColumn get projectionType => $composableBuilder( + column: $table.projectionType, builder: (column) => column); + + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteExifEntityTable, + i1.RemoteExifEntityData, + i1.$$RemoteExifEntityTableFilterComposer, + i1.$$RemoteExifEntityTableOrderingComposer, + i1.$$RemoteExifEntityTableAnnotationComposer, + $$RemoteExifEntityTableCreateCompanionBuilder, + $$RemoteExifEntityTableUpdateCompanionBuilder, + (i1.RemoteExifEntityData, i1.$$RemoteExifEntityTableReferences), + i1.RemoteExifEntityData, + i0.PrefetchHooks Function({bool assetId})> { + $$RemoteExifEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteExifEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteExifEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteExifEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteExifEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent(), + }) => + i1.RemoteExifEntityCompanion( + assetId: assetId, + city: city, + state: state, + country: country, + dateTimeOriginal: dateTimeOriginal, + description: description, + height: height, + width: width, + exposureTime: exposureTime, + fNumber: fNumber, + fileSize: fileSize, + focalLength: focalLength, + latitude: latitude, + longitude: longitude, + iso: iso, + make: make, + model: model, + orientation: orientation, + timeZone: timeZone, + rating: rating, + projectionType: projectionType, + ), + createCompanionCallback: ({ + required String assetId, + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent(), + }) => + i1.RemoteExifEntityCompanion.insert( + assetId: assetId, + city: city, + state: state, + country: country, + dateTimeOriginal: dateTimeOriginal, + description: description, + height: height, + width: width, + exposureTime: exposureTime, + fNumber: fNumber, + fileSize: fileSize, + focalLength: focalLength, + latitude: latitude, + longitude: longitude, + iso: iso, + make: make, + model: model, + orientation: orientation, + timeZone: timeZone, + rating: rating, + projectionType: projectionType, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteExifEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: + i1.$$RemoteExifEntityTableReferences._assetIdTable(db), + referencedColumn: i1.$$RemoteExifEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteExifEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteExifEntityTable, + i1.RemoteExifEntityData, + i1.$$RemoteExifEntityTableFilterComposer, + i1.$$RemoteExifEntityTableOrderingComposer, + i1.$$RemoteExifEntityTableAnnotationComposer, + $$RemoteExifEntityTableCreateCompanionBuilder, + $$RemoteExifEntityTableUpdateCompanionBuilder, + (i1.RemoteExifEntityData, i1.$$RemoteExifEntityTableReferences), + i1.RemoteExifEntityData, + i0.PrefetchHooks Function({bool assetId})>; + +class $RemoteExifEntityTable extends i2.RemoteExifEntity + with i0.TableInfo<$RemoteExifEntityTable, i1.RemoteExifEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteExifEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _cityMeta = + const i0.VerificationMeta('city'); + @override + late final i0.GeneratedColumn city = i0.GeneratedColumn( + 'city', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _stateMeta = + const i0.VerificationMeta('state'); + @override + late final i0.GeneratedColumn state = i0.GeneratedColumn( + 'state', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _countryMeta = + const i0.VerificationMeta('country'); + @override + late final i0.GeneratedColumn country = i0.GeneratedColumn( + 'country', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _dateTimeOriginalMeta = + const i0.VerificationMeta('dateTimeOriginal'); + @override + late final i0.GeneratedColumn dateTimeOriginal = + i0.GeneratedColumn('date_time_original', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _descriptionMeta = + const i0.VerificationMeta('description'); + @override + late final i0.GeneratedColumn description = + i0.GeneratedColumn('description', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _heightMeta = + const i0.VerificationMeta('height'); + @override + late final i0.GeneratedColumn height = i0.GeneratedColumn( + 'height', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _widthMeta = + const i0.VerificationMeta('width'); + @override + late final i0.GeneratedColumn width = i0.GeneratedColumn( + 'width', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _exposureTimeMeta = + const i0.VerificationMeta('exposureTime'); + @override + late final i0.GeneratedColumn exposureTime = + i0.GeneratedColumn('exposure_time', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _fNumberMeta = + const i0.VerificationMeta('fNumber'); + @override + late final i0.GeneratedColumn fNumber = i0.GeneratedColumn( + 'f_number', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _fileSizeMeta = + const i0.VerificationMeta('fileSize'); + @override + late final i0.GeneratedColumn fileSize = i0.GeneratedColumn( + 'file_size', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _focalLengthMeta = + const i0.VerificationMeta('focalLength'); + @override + late final i0.GeneratedColumn focalLength = i0.GeneratedColumn( + 'focal_length', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _latitudeMeta = + const i0.VerificationMeta('latitude'); + @override + late final i0.GeneratedColumn latitude = i0.GeneratedColumn( + 'latitude', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _longitudeMeta = + const i0.VerificationMeta('longitude'); + @override + late final i0.GeneratedColumn longitude = i0.GeneratedColumn( + 'longitude', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _isoMeta = const i0.VerificationMeta('iso'); + @override + late final i0.GeneratedColumn iso = i0.GeneratedColumn( + 'iso', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _makeMeta = + const i0.VerificationMeta('make'); + @override + late final i0.GeneratedColumn make = i0.GeneratedColumn( + 'make', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _modelMeta = + const i0.VerificationMeta('model'); + @override + late final i0.GeneratedColumn model = i0.GeneratedColumn( + 'model', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _orientationMeta = + const i0.VerificationMeta('orientation'); + @override + late final i0.GeneratedColumn orientation = + i0.GeneratedColumn('orientation', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _timeZoneMeta = + const i0.VerificationMeta('timeZone'); + @override + late final i0.GeneratedColumn timeZone = i0.GeneratedColumn( + 'time_zone', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _ratingMeta = + const i0.VerificationMeta('rating'); + @override + late final i0.GeneratedColumn rating = i0.GeneratedColumn( + 'rating', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _projectionTypeMeta = + const i0.VerificationMeta('projectionType'); + @override + late final i0.GeneratedColumn projectionType = + i0.GeneratedColumn('projection_type', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('city')) { + context.handle( + _cityMeta, city.isAcceptableOrUnknown(data['city']!, _cityMeta)); + } + if (data.containsKey('state')) { + context.handle( + _stateMeta, state.isAcceptableOrUnknown(data['state']!, _stateMeta)); + } + if (data.containsKey('country')) { + context.handle(_countryMeta, + country.isAcceptableOrUnknown(data['country']!, _countryMeta)); + } + if (data.containsKey('date_time_original')) { + context.handle( + _dateTimeOriginalMeta, + dateTimeOriginal.isAcceptableOrUnknown( + data['date_time_original']!, _dateTimeOriginalMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } + if (data.containsKey('width')) { + context.handle( + _widthMeta, width.isAcceptableOrUnknown(data['width']!, _widthMeta)); + } + if (data.containsKey('exposure_time')) { + context.handle( + _exposureTimeMeta, + exposureTime.isAcceptableOrUnknown( + data['exposure_time']!, _exposureTimeMeta)); + } + if (data.containsKey('f_number')) { + context.handle(_fNumberMeta, + fNumber.isAcceptableOrUnknown(data['f_number']!, _fNumberMeta)); + } + if (data.containsKey('file_size')) { + context.handle(_fileSizeMeta, + fileSize.isAcceptableOrUnknown(data['file_size']!, _fileSizeMeta)); + } + if (data.containsKey('focal_length')) { + context.handle( + _focalLengthMeta, + focalLength.isAcceptableOrUnknown( + data['focal_length']!, _focalLengthMeta)); + } + if (data.containsKey('latitude')) { + context.handle(_latitudeMeta, + latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta)); + } + if (data.containsKey('longitude')) { + context.handle(_longitudeMeta, + longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta)); + } + if (data.containsKey('iso')) { + context.handle( + _isoMeta, iso.isAcceptableOrUnknown(data['iso']!, _isoMeta)); + } + if (data.containsKey('make')) { + context.handle( + _makeMeta, make.isAcceptableOrUnknown(data['make']!, _makeMeta)); + } + if (data.containsKey('model')) { + context.handle( + _modelMeta, model.isAcceptableOrUnknown(data['model']!, _modelMeta)); + } + if (data.containsKey('orientation')) { + context.handle( + _orientationMeta, + orientation.isAcceptableOrUnknown( + data['orientation']!, _orientationMeta)); + } + if (data.containsKey('time_zone')) { + context.handle(_timeZoneMeta, + timeZone.isAcceptableOrUnknown(data['time_zone']!, _timeZoneMeta)); + } + if (data.containsKey('rating')) { + context.handle(_ratingMeta, + rating.isAcceptableOrUnknown(data['rating']!, _ratingMeta)); + } + if (data.containsKey('projection_type')) { + context.handle( + _projectionTypeMeta, + projectionType.isAcceptableOrUnknown( + data['projection_type']!, _projectionTypeMeta)); + } + return context; + } + + @override + Set get $primaryKey => {assetId}; + @override + i1.RemoteExifEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteExifEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}model']), + orientation: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + $RemoteExifEntityTable createAlias(String alias) { + return $RemoteExifEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final int? fNumber; + final int? fileSize; + final int? focalLength; + final int? latitude; + final int? longitude; + final int? iso; + final String? make; + final String? model; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = i0.Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = i0.Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = i0.Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = i0.Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = i0.Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = i0.Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = i0.Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = i0.Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = i0.Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = i0.Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = i0.Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = i0.Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = i0.Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = i0.Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = i0.Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = i0.Variable(model); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = i0.Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = i0.Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = i0.Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = i0.Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: + serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + i1.RemoteExifEntityData copyWith( + {String? assetId, + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent()}) => + i1.RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: + exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: + projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(i1.RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: + data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: + data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value city; + final i0.Value state; + final i0.Value country; + final i0.Value dateTimeOriginal; + final i0.Value description; + final i0.Value height; + final i0.Value width; + final i0.Value exposureTime; + final i0.Value fNumber; + final i0.Value fileSize; + final i0.Value focalLength; + final i0.Value latitude; + final i0.Value longitude; + final i0.Value iso; + final i0.Value make; + final i0.Value model; + final i0.Value orientation; + final i0.Value timeZone; + final i0.Value rating; + final i0.Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.city = const i0.Value.absent(), + this.state = const i0.Value.absent(), + this.country = const i0.Value.absent(), + this.dateTimeOriginal = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.exposureTime = const i0.Value.absent(), + this.fNumber = const i0.Value.absent(), + this.fileSize = const i0.Value.absent(), + this.focalLength = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + this.iso = const i0.Value.absent(), + this.make = const i0.Value.absent(), + this.model = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), + this.timeZone = const i0.Value.absent(), + this.rating = const i0.Value.absent(), + this.projectionType = const i0.Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const i0.Value.absent(), + this.state = const i0.Value.absent(), + this.country = const i0.Value.absent(), + this.dateTimeOriginal = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.exposureTime = const i0.Value.absent(), + this.fNumber = const i0.Value.absent(), + this.fileSize = const i0.Value.absent(), + this.focalLength = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + this.iso = const i0.Value.absent(), + this.make = const i0.Value.absent(), + this.model = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), + this.timeZone = const i0.Value.absent(), + this.rating = const i0.Value.absent(), + this.projectionType = const i0.Value.absent(), + }) : assetId = i0.Value(assetId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? city, + i0.Expression? state, + i0.Expression? country, + i0.Expression? dateTimeOriginal, + i0.Expression? description, + i0.Expression? height, + i0.Expression? width, + i0.Expression? exposureTime, + i0.Expression? fNumber, + i0.Expression? fileSize, + i0.Expression? focalLength, + i0.Expression? latitude, + i0.Expression? longitude, + i0.Expression? iso, + i0.Expression? make, + i0.Expression? model, + i0.Expression? orientation, + i0.Expression? timeZone, + i0.Expression? rating, + i0.Expression? projectionType, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + i1.RemoteExifEntityCompanion copyWith( + {i0.Value? assetId, + i0.Value? city, + i0.Value? state, + i0.Value? country, + i0.Value? dateTimeOriginal, + i0.Value? description, + i0.Value? height, + i0.Value? width, + i0.Value? exposureTime, + i0.Value? fNumber, + i0.Value? fileSize, + i0.Value? focalLength, + i0.Value? latitude, + i0.Value? longitude, + i0.Value? iso, + i0.Value? make, + i0.Value? model, + i0.Value? orientation, + i0.Value? timeZone, + i0.Value? rating, + i0.Value? projectionType}) { + return i1.RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (city.present) { + map['city'] = i0.Variable(city.value); + } + if (state.present) { + map['state'] = i0.Variable(state.value); + } + if (country.present) { + map['country'] = i0.Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = i0.Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = i0.Variable(description.value); + } + if (height.present) { + map['height'] = i0.Variable(height.value); + } + if (width.present) { + map['width'] = i0.Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = i0.Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = i0.Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = i0.Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = i0.Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = i0.Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = i0.Variable(longitude.value); + } + if (iso.present) { + map['iso'] = i0.Variable(iso.value); + } + if (make.present) { + map['make'] = i0.Variable(make.value); + } + if (model.present) { + map['model'] = i0.Variable(model.value); + } + if (orientation.present) { + map['orientation'] = i0.Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = i0.Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = i0.Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = i0.Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart new file mode 100644 index 00000000000..74c3e7a8f73 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumEntity extends Table with DriftDefaultsMixin { + const LocalAlbumEntity(); + + TextColumn get id => text()(); + TextColumn get name => text()(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get backupSelection => intEnum()(); + + // Used for mark & sweep + BoolColumn get marker_ => boolean().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart new file mode 100644 index 00000000000..5955742ec0e --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart @@ -0,0 +1,497 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/local_album.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$LocalAlbumEntityTableCreateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + required String id, + required String name, + i0.Value updatedAt, + required i2.BackupSelection backupSelection, + i0.Value marker_, +}); +typedef $$LocalAlbumEntityTableUpdateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value updatedAt, + i0.Value backupSelection, + i0.Value marker_, +}); + +class $$LocalAlbumEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get marker_ => $composableBuilder( + column: $table.marker_, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LocalAlbumEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get marker_ => $composableBuilder( + column: $table.marker_, builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LocalAlbumEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter + get backupSelection => $composableBuilder( + column: $table.backupSelection, builder: (column) => column); + + i0.GeneratedColumn get marker_ => + $composableBuilder(column: $table.marker_, builder: (column) => column); +} + +class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumEntityData, + i0.BaseReferences + ), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function()> { + $$LocalAlbumEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAlbumEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value backupSelection = + const i0.Value.absent(), + i0.Value marker_ = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion( + id: id, + name: name, + updatedAt: updatedAt, + backupSelection: backupSelection, + marker_: marker_, + ), + createCompanionCallback: ({ + required String id, + required String name, + i0.Value updatedAt = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + i0.Value marker_ = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion.insert( + id: id, + name: name, + updatedAt: updatedAt, + backupSelection: backupSelection, + marker_: marker_, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumEntityData, + i0.BaseReferences + ), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function()>; + +class $LocalAlbumEntityTable extends i3.LocalAlbumEntity + with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + @override + late final i0.GeneratedColumnWithTypeConverter + backupSelection = i0.GeneratedColumn( + 'backup_selection', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAlbumEntityTable.$converterbackupSelection); + static const i0.VerificationMeta _marker_Meta = + const i0.VerificationMeta('marker_'); + @override + late final i0.GeneratedColumn marker_ = i0.GeneratedColumn( + 'marker', aliasedName, true, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => + [id, name, updatedAt, backupSelection, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('marker')) { + context.handle(_marker_Meta, + marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LocalAlbumEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int, + data['${effectivePrefix}backup_selection'])!), + marker_: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + $LocalAlbumEntityTable createAlias(String alias) { + return $LocalAlbumEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 + $converterbackupSelection = + const i0.EnumIndexConverter( + i2.BackupSelection.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final DateTime updatedAt; + final i2.BackupSelection backupSelection; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['updated_at'] = i0.Variable(updatedAt); + { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection)); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = i0.Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromJson(serializer.fromJson(json['backupSelection'])), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toJson(backupSelection)), + 'marker_': serializer.toJson(marker_), + }; + } + + i1.LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + i2.BackupSelection? backupSelection, + i0.Value marker_ = const i0.Value.absent()}) => + i1.LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, updatedAt, backupSelection, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value updatedAt; + final i0.Value backupSelection; + final i0.Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.backupSelection = const i0.Value.absent(), + this.marker_ = const i0.Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + this.marker_ = const i0.Value.absent(), + }) : id = i0.Value(id), + name = i0.Value(name), + backupSelection = i0.Value(backupSelection); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? updatedAt, + i0.Expression? backupSelection, + i0.Expression? marker_, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (marker_ != null) 'marker': marker_, + }); + } + + i1.LocalAlbumEntityCompanion copyWith( + {i0.Value? id, + i0.Value? name, + i0.Value? updatedAt, + i0.Value? backupSelection, + i0.Value? marker_}) { + return i1.LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection.value)); + } + if (marker_.present) { + map['marker'] = i0.Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart new file mode 100644 index 00000000000..b64b9ec2fbe --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { + const LocalAlbumAssetEntity(); + + TextColumn get assetId => + text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => + text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart new file mode 100644 index 00000000000..e8f94fa74b4 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -0,0 +1,565 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i5; + +typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + required String assetId, + required String albumId, +}); +typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value albumId, +}); + +final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData> { + $$LocalAlbumAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$LocalAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .id)); + + i3.$$LocalAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$LocalAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$LocalAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .albumId, + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .id)); + + i5.$$LocalAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i5 + .$$LocalAlbumEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$LocalAlbumAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableFilterComposer get assetId { + final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableFilterComposer get albumId { + final i5.$$LocalAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableOrderingComposer get assetId { + final i3.$$LocalAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableOrderingComposer get albumId { + final i5.$$LocalAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableAnnotationComposer get assetId { + final i3.$$LocalAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableAnnotationComposer get albumId { + final i5.$$LocalAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + (i1.LocalAlbumAssetEntityData, i1.$$LocalAlbumAssetEntityTableReferences), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})> { + $$LocalAlbumAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumAssetEntityTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + i1.$$LocalAlbumAssetEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value albumId = const i0.Value.absent(), + }) => + i1.LocalAlbumAssetEntityCompanion( + assetId: assetId, + albumId: albumId, + ), + createCompanionCallback: ({ + required String assetId, + required String albumId, + }) => + i1.LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$LocalAlbumAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, albumId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$LocalAlbumAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableReferences + ), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})>; + +class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity + with + i0 + .TableInfo<$LocalAlbumAssetEntityTable, i1.LocalAlbumAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, albumId}; + @override + i1.LocalAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + $LocalAlbumAssetEntityTable createAlias(String alias) { + return $LocalAlbumAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + map['album_id'] = i0.Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + i1.LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + i1.LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.albumId = const i0.Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = i0.Value(assetId), + albumId = i0.Value(albumId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? albumId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + i1.LocalAlbumAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? albumId}) { + return i1.LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart new file mode 100644 index 00000000000..ff5ee748181 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex(name: 'idx_local_asset_checksum', columns: {#checksum}) +class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { + const LocalAssetEntity(); + + TextColumn get id => text()(); + TextColumn get checksum => text().nullable()(); + + // Only used during backup to mirror the favorite status of the asset in the server + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart new file mode 100644 index 00000000000..68bc1b3c5dd --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -0,0 +1,658 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$LocalAssetEntityTableCreateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + required String name, + required i2.AssetType type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + required String id, + i0.Value checksum, + i0.Value isFavorite, +}); +typedef $$LocalAssetEntityTableUpdateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + i0.Value name, + i0.Value type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + i0.Value id, + i0.Value checksum, + i0.Value isFavorite, +}); + +class $$LocalAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LocalAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LocalAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, builder: (column) => column); + + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get checksum => + $composableBuilder(column: $table.checksum, builder: (column) => column); + + i0.GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => column); +} + +class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()> { + $$LocalAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value name = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ), + createCompanionCallback: ({ + required String name, + required i2.AssetType type, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + required String id, + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion.insert( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()>; +i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + +class $LocalAssetEntityTable extends i3.LocalAssetEntity + with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAssetEntityTable.$convertertype); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _durationInSecondsMeta = + const i0.VerificationMeta('durationInSeconds'); + @override + late final i0.GeneratedColumn durationInSeconds = + i0.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _checksumMeta = + const i0.VerificationMeta('checksum'); + @override + late final i0.GeneratedColumn checksum = i0.GeneratedColumn( + 'checksum', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _isFavoriteMeta = + const i0.VerificationMeta('isFavorite'); + @override + late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn( + 'is_favorite', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('duration_in_seconds')) { + context.handle( + _durationInSecondsMeta, + durationInSeconds.isAcceptableOrUnknown( + data['duration_in_seconds']!, _durationInSecondsMeta)); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('checksum')) { + context.handle(_checksumMeta, + checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); + } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown( + data['is_favorite']!, _isFavoriteMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LocalAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAssetEntityData( + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + type: i1.$LocalAssetEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + durationInSeconds: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ); + } + + @override + $LocalAssetEntityTable createAlias(String alias) { + return $LocalAssetEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.AssetType.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String name; + final i2.AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type)); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds); + } + map['id'] = i0.Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = i0.Variable(checksum); + } + map['is_favorite'] = i0.Variable(isFavorite); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: i1.$LocalAssetEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer + .toJson(i1.$LocalAssetEntityTable.$convertertype.toJson(type)), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + }; + } + + i1.LocalAssetEntityData copyWith( + {String? name, + i2.AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value durationInSeconds = const i0.Value.absent(), + String? id, + i0.Value checksum = const i0.Value.absent(), + bool? isFavorite}) => + i1.LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ); + LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, + durationInSeconds, id, checksum, isFavorite); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite); +} + +class LocalAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value name; + final i0.Value type; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value durationInSeconds; + final i0.Value id; + final i0.Value checksum; + final i0.Value isFavorite; + const LocalAssetEntityCompanion({ + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + this.id = const i0.Value.absent(), + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required i2.AssetType type, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + required String id, + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + }) : name = i0.Value(name), + type = i0.Value(type), + id = i0.Value(id); + static i0.Insertable custom({ + i0.Expression? name, + i0.Expression? type, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? durationInSeconds, + i0.Expression? id, + i0.Expression? checksum, + i0.Expression? isFavorite, + }) { + return i0.RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + }); + } + + i1.LocalAssetEntityCompanion copyWith( + {i0.Value? name, + i0.Value? type, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? durationInSeconds, + i0.Value? id, + i0.Value? checksum, + i0.Value? isFavorite}) { + return i1.LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (checksum.present) { + map['checksum'] = i0.Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart index b7925a8eeaf..8b51d93e6f7 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -5,11 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class PartnerEntity extends Table with DriftDefaultsMixin { const PartnerEntity(); - BlobColumn get sharedById => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedById => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); - BlobColumn get sharedWithId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedWithId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); BoolColumn get inTimeline => boolean().withDefault(const Constant(false))(); diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart index 974a9e3c303..26a5dd2fe0e 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.drift.dart @@ -3,24 +3,23 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart' - as i3; -import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' - as i5; -import 'package:drift/internal/modular.dart' as i6; + as i4; +import 'package:drift/internal/modular.dart' as i5; typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion Function({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, i0.Value inTimeline, }); typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion Function({ - i0.Value sharedById, - i0.Value sharedWithId, + i0.Value sharedById, + i0.Value sharedWithId, i0.Value inTimeline, }); @@ -29,25 +28,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences< $$PartnerEntityTableReferences( super.$_db, super.$_table, super.$_typedResult); - static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('partner_entity') .sharedById, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get sharedById { - final $_column = $_itemColumn('shared_by_id')!; + i4.$$UserEntityTableProcessedTableManager get sharedById { + final $_column = $_itemColumn('shared_by_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db)); if (item == null) return manager; @@ -55,25 +54,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences< manager.$state.copyWith(prefetchedData: [item])); } - static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('partner_entity') .sharedWithId, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get sharedWithId { - final $_column = $_itemColumn('shared_with_id')!; + i4.$$UserEntityTableProcessedTableManager get sharedWithId { + final $_column = $_itemColumn('shared_with_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db)); if (item == null) return manager; @@ -94,20 +93,20 @@ class $$PartnerEntityTableFilterComposer i0.ColumnFilters get inTimeline => $composableBuilder( column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column)); - i5.$$UserEntityTableFilterComposer get sharedById { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get sharedById { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -116,20 +115,20 @@ class $$PartnerEntityTableFilterComposer return composer; } - i5.$$UserEntityTableFilterComposer get sharedWithId { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get sharedWithId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -152,20 +151,20 @@ class $$PartnerEntityTableOrderingComposer column: $table.inTimeline, builder: (column) => i0.ColumnOrderings(column)); - i5.$$UserEntityTableOrderingComposer get sharedById { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get sharedById { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -174,20 +173,20 @@ class $$PartnerEntityTableOrderingComposer return composer; } - i5.$$UserEntityTableOrderingComposer get sharedWithId { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get sharedWithId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -209,20 +208,20 @@ class $$PartnerEntityTableAnnotationComposer i0.GeneratedColumn get inTimeline => $composableBuilder( column: $table.inTimeline, builder: (column) => column); - i5.$$UserEntityTableAnnotationComposer get sharedById { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get sharedById { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -231,20 +230,20 @@ class $$PartnerEntityTableAnnotationComposer return composer; } - i5.$$UserEntityTableAnnotationComposer get sharedWithId { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get sharedWithId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -278,8 +277,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager< createComputedFieldComposer: () => i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - i0.Value sharedById = const i0.Value.absent(), - i0.Value sharedWithId = const i0.Value.absent(), + i0.Value sharedById = const i0.Value.absent(), + i0.Value sharedWithId = const i0.Value.absent(), i0.Value inTimeline = const i0.Value.absent(), }) => i1.PartnerEntityCompanion( @@ -288,8 +287,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager< inTimeline: inTimeline, ), createCompanionCallback: ({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, i0.Value inTimeline = const i0.Value.absent(), }) => i1.PartnerEntityCompanion.insert( @@ -366,7 +365,7 @@ typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager< i1.PartnerEntityData, i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>; -class $PartnerEntityTable extends i3.PartnerEntity +class $PartnerEntityTable extends i2.PartnerEntity with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -375,18 +374,18 @@ class $PartnerEntityTable extends i3.PartnerEntity static const i0.VerificationMeta _sharedByIdMeta = const i0.VerificationMeta('sharedById'); @override - late final i0.GeneratedColumn sharedById = - i0.GeneratedColumn('shared_by_id', aliasedName, false, - type: i0.DriftSqlType.blob, - requiredDuringInsert: true, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final i0.GeneratedColumn sharedById = i0.GeneratedColumn( + 'shared_by_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); static const i0.VerificationMeta _sharedWithIdMeta = const i0.VerificationMeta('sharedWithId'); @override - late final i0.GeneratedColumn sharedWithId = - i0.GeneratedColumn('shared_with_id', aliasedName, false, - type: i0.DriftSqlType.blob, + late final i0.GeneratedColumn sharedWithId = + i0.GeneratedColumn('shared_with_id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'REFERENCES user_entity (id) ON DELETE CASCADE')); @@ -399,7 +398,7 @@ class $PartnerEntityTable extends i3.PartnerEntity requiredDuringInsert: false, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'CHECK ("in_timeline" IN (0, 1))'), - defaultValue: const i4.Constant(false)); + defaultValue: const i3.Constant(false)); @override List get $columns => [sharedById, sharedWithId, inTimeline]; @@ -445,10 +444,10 @@ class $PartnerEntityTable extends i3.PartnerEntity i1.PartnerEntityData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.PartnerEntityData( - sharedById: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!, + sharedById: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, sharedWithId: attachedDatabase.typeMapping.read( - i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!, + i0.DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, inTimeline: attachedDatabase.typeMapping .read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, ); @@ -467,8 +466,8 @@ class $PartnerEntityTable extends i3.PartnerEntity class PartnerEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List sharedById; - final i2.Uint8List sharedWithId; + final String sharedById; + final String sharedWithId; final bool inTimeline; const PartnerEntityData( {required this.sharedById, @@ -477,8 +476,8 @@ class PartnerEntityData extends i0.DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; - map['shared_by_id'] = i0.Variable(sharedById); - map['shared_with_id'] = i0.Variable(sharedWithId); + map['shared_by_id'] = i0.Variable(sharedById); + map['shared_with_id'] = i0.Variable(sharedWithId); map['in_timeline'] = i0.Variable(inTimeline); return map; } @@ -487,8 +486,8 @@ class PartnerEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return PartnerEntityData( - sharedById: serializer.fromJson(json['sharedById']), - sharedWithId: serializer.fromJson(json['sharedWithId']), + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), inTimeline: serializer.fromJson(json['inTimeline']), ); } @@ -496,16 +495,14 @@ class PartnerEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'sharedById': serializer.toJson(sharedById), - 'sharedWithId': serializer.toJson(sharedWithId), + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), 'inTimeline': serializer.toJson(inTimeline), }; } i1.PartnerEntityData copyWith( - {i2.Uint8List? sharedById, - i2.Uint8List? sharedWithId, - bool? inTimeline}) => + {String? sharedById, String? sharedWithId, bool? inTimeline}) => i1.PartnerEntityData( sharedById: sharedById ?? this.sharedById, sharedWithId: sharedWithId ?? this.sharedWithId, @@ -534,20 +531,19 @@ class PartnerEntityData extends i0.DataClass } @override - int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById), - i0.$driftBlobEquality.hash(sharedWithId), inTimeline); + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); @override bool operator ==(Object other) => identical(this, other) || (other is i1.PartnerEntityData && - i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) && - i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && other.inTimeline == this.inTimeline); } class PartnerEntityCompanion extends i0.UpdateCompanion { - final i0.Value sharedById; - final i0.Value sharedWithId; + final i0.Value sharedById; + final i0.Value sharedWithId; final i0.Value inTimeline; const PartnerEntityCompanion({ this.sharedById = const i0.Value.absent(), @@ -555,14 +551,14 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { this.inTimeline = const i0.Value.absent(), }); PartnerEntityCompanion.insert({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, this.inTimeline = const i0.Value.absent(), }) : sharedById = i0.Value(sharedById), sharedWithId = i0.Value(sharedWithId); static i0.Insertable custom({ - i0.Expression? sharedById, - i0.Expression? sharedWithId, + i0.Expression? sharedById, + i0.Expression? sharedWithId, i0.Expression? inTimeline, }) { return i0.RawValuesInsertable({ @@ -573,8 +569,8 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { } i1.PartnerEntityCompanion copyWith( - {i0.Value? sharedById, - i0.Value? sharedWithId, + {i0.Value? sharedById, + i0.Value? sharedWithId, i0.Value? inTimeline}) { return i1.PartnerEntityCompanion( sharedById: sharedById ?? this.sharedById, @@ -587,10 +583,10 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (sharedById.present) { - map['shared_by_id'] = i0.Variable(sharedById.value); + map['shared_by_id'] = i0.Variable(sharedById.value); } if (sharedWithId.present) { - map['shared_with_id'] = i0.Variable(sharedWithId.value); + map['shared_with_id'] = i0.Variable(sharedWithId.value); } if (inTimeline.present) { map['in_timeline'] = i0.Variable(inTimeline.value); diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart new file mode 100644 index 00000000000..96f4077a2ae --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -0,0 +1,35 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex( + name: 'UQ_remote_asset_owner_checksum', + columns: {#checksum, #ownerId}, + unique: true, +) +class RemoteAssetEntity extends Table + with DriftDefaultsMixin, AssetEntityMixin { + const RemoteAssetEntity(); + + TextColumn get id => text()(); + + TextColumn get checksum => text()(); + + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + DateTimeColumn get localDateTime => dateTime().nullable()(); + + TextColumn get thumbHash => text().nullable()(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + IntColumn get visibility => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart new file mode 100644 index 00000000000..e3fe521700e --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -0,0 +1,1076 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$RemoteAssetEntityTableCreateCompanionBuilder + = i1.RemoteAssetEntityCompanion Function({ + required String name, + required i2.AssetType type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + required String id, + required String checksum, + i0.Value isFavorite, + required String ownerId, + i0.Value localDateTime, + i0.Value thumbHash, + i0.Value deletedAt, + required i2.AssetVisibility visibility, +}); +typedef $$RemoteAssetEntityTableUpdateCompanionBuilder + = i1.RemoteAssetEntityCompanion Function({ + i0.Value name, + i0.Value type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + i0.Value id, + i0.Value checksum, + i0.Value isFavorite, + i0.Value ownerId, + i0.Value localDateTime, + i0.Value thumbHash, + i0.Value deletedAt, + i0.Value visibility, +}); + +final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData> { + $$RemoteAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .ownerId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAssetEntityTableFilterComposer + extends i0.Composer { + $$RemoteAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get localDateTime => $composableBuilder( + column: $table.localDateTime, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get thumbHash => $composableBuilder( + column: $table.thumbHash, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i5.$$UserEntityTableFilterComposer get ownerId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get localDateTime => $composableBuilder( + column: $table.localDateTime, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get thumbHash => $composableBuilder( + column: $table.thumbHash, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get ownerId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, builder: (column) => column); + + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get checksum => + $composableBuilder(column: $table.checksum, builder: (column) => column); + + i0.GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => column); + + i0.GeneratedColumn get localDateTime => $composableBuilder( + column: $table.localDateTime, builder: (column) => column); + + i0.GeneratedColumn get thumbHash => + $composableBuilder(column: $table.thumbHash, builder: (column) => column); + + i0.GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get visibility => + $composableBuilder( + column: $table.visibility, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get ownerId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData, + i1.$$RemoteAssetEntityTableFilterComposer, + i1.$$RemoteAssetEntityTableOrderingComposer, + i1.$$RemoteAssetEntityTableAnnotationComposer, + $$RemoteAssetEntityTableCreateCompanionBuilder, + $$RemoteAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAssetEntityData, i1.$$RemoteAssetEntityTableReferences), + i1.RemoteAssetEntityData, + i0.PrefetchHooks Function({bool ownerId})> { + $$RemoteAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value name = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i0.Value visibility = const i0.Value.absent(), + }) => + i1.RemoteAssetEntityCompanion( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ownerId: ownerId, + localDateTime: localDateTime, + thumbHash: thumbHash, + deletedAt: deletedAt, + visibility: visibility, + ), + createCompanionCallback: ({ + required String name, + required i2.AssetType type, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + required String id, + required String checksum, + i0.Value isFavorite = const i0.Value.absent(), + required String ownerId, + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + required i2.AssetVisibility visibility, + }) => + i1.RemoteAssetEntityCompanion.insert( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ownerId: ownerId, + localDateTime: localDateTime, + thumbHash: thumbHash, + deletedAt: deletedAt, + visibility: visibility, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$RemoteAssetEntityTableReferences._ownerIdTable(db), + referencedColumn: i1.$$RemoteAssetEntityTableReferences + ._ownerIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData, + i1.$$RemoteAssetEntityTableFilterComposer, + i1.$$RemoteAssetEntityTableOrderingComposer, + i1.$$RemoteAssetEntityTableAnnotationComposer, + $$RemoteAssetEntityTableCreateCompanionBuilder, + $$RemoteAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAssetEntityData, i1.$$RemoteAssetEntityTableReferences), + i1.RemoteAssetEntityData, + i0.PrefetchHooks Function({bool ownerId})>; +i0.Index get uQRemoteAssetOwnerChecksum => i0.Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + +class $RemoteAssetEntityTable extends i3.RemoteAssetEntity + with i0.TableInfo<$RemoteAssetEntityTable, i1.RemoteAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAssetEntityTable.$convertertype); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _durationInSecondsMeta = + const i0.VerificationMeta('durationInSeconds'); + @override + late final i0.GeneratedColumn durationInSeconds = + i0.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _checksumMeta = + const i0.VerificationMeta('checksum'); + @override + late final i0.GeneratedColumn checksum = i0.GeneratedColumn( + 'checksum', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _isFavoriteMeta = + const i0.VerificationMeta('isFavorite'); + @override + late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn( + 'is_favorite', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _localDateTimeMeta = + const i0.VerificationMeta('localDateTime'); + @override + late final i0.GeneratedColumn localDateTime = + i0.GeneratedColumn('local_date_time', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _thumbHashMeta = + const i0.VerificationMeta('thumbHash'); + @override + late final i0.GeneratedColumn thumbHash = i0.GeneratedColumn( + 'thumb_hash', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _deletedAtMeta = + const i0.VerificationMeta('deletedAt'); + @override + late final i0.GeneratedColumn deletedAt = + i0.GeneratedColumn('deleted_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + @override + late final i0.GeneratedColumnWithTypeConverter + visibility = i0.GeneratedColumn('visibility', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAssetEntityTable.$convertervisibility); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + visibility + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('duration_in_seconds')) { + context.handle( + _durationInSecondsMeta, + durationInSeconds.isAcceptableOrUnknown( + data['duration_in_seconds']!, _durationInSecondsMeta)); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('checksum')) { + context.handle(_checksumMeta, + checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); + } else if (isInserting) { + context.missing(_checksumMeta); + } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown( + data['is_favorite']!, _isFavoriteMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('local_date_time')) { + context.handle( + _localDateTimeMeta, + localDateTime.isAcceptableOrUnknown( + data['local_date_time']!, _localDateTimeMeta)); + } + if (data.containsKey('thumb_hash')) { + context.handle(_thumbHashMeta, + thumbHash.isAcceptableOrUnknown(data['thumb_hash']!, _thumbHashMeta)); + } + if (data.containsKey('deleted_at')) { + context.handle(_deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.RemoteAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAssetEntityData( + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + type: i1.$RemoteAssetEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + durationInSeconds: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), + ); + } + + @override + $RemoteAssetEntityTable createAlias(String alias) { + return $RemoteAssetEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.AssetType.values); + static i0.JsonTypeConverter2 + $convertervisibility = const i0.EnumIndexConverter( + i2.AssetVisibility.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String name; + final i2.AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final i2.AssetVisibility visibility; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + required this.visibility}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertertype.toSql(type)); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds); + } + map['id'] = i0.Variable(id); + map['checksum'] = i0.Variable(checksum); + map['is_favorite'] = i0.Variable(isFavorite); + map['owner_id'] = i0.Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = i0.Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = i0.Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = i0.Variable(deletedAt); + } + { + map['visibility'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: i1.$RemoteAssetEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + visibility: i1.$RemoteAssetEntityTable.$convertervisibility + .fromJson(serializer.fromJson(json['visibility'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer + .toJson(i1.$RemoteAssetEntityTable.$convertertype.toJson(type)), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'visibility': serializer.toJson( + i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), + }; + } + + i1.RemoteAssetEntityData copyWith( + {String? name, + i2.AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value durationInSeconds = const i0.Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i2.AssetVisibility? visibility}) => + i1.RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: + localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + visibility: visibility ?? this.visibility, + ); + RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + visibility: + data.visibility.present ? data.visibility.value : this.visibility, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('visibility: $visibility') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + visibility); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.visibility == this.visibility); +} + +class RemoteAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value name; + final i0.Value type; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value durationInSeconds; + final i0.Value id; + final i0.Value checksum; + final i0.Value isFavorite; + final i0.Value ownerId; + final i0.Value localDateTime; + final i0.Value thumbHash; + final i0.Value deletedAt; + final i0.Value visibility; + const RemoteAssetEntityCompanion({ + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + this.id = const i0.Value.absent(), + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.localDateTime = const i0.Value.absent(), + this.thumbHash = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + this.visibility = const i0.Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required i2.AssetType type, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + required String id, + required String checksum, + this.isFavorite = const i0.Value.absent(), + required String ownerId, + this.localDateTime = const i0.Value.absent(), + this.thumbHash = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + required i2.AssetVisibility visibility, + }) : name = i0.Value(name), + type = i0.Value(type), + id = i0.Value(id), + checksum = i0.Value(checksum), + ownerId = i0.Value(ownerId), + visibility = i0.Value(visibility); + static i0.Insertable custom({ + i0.Expression? name, + i0.Expression? type, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? durationInSeconds, + i0.Expression? id, + i0.Expression? checksum, + i0.Expression? isFavorite, + i0.Expression? ownerId, + i0.Expression? localDateTime, + i0.Expression? thumbHash, + i0.Expression? deletedAt, + i0.Expression? visibility, + }) { + return i0.RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (visibility != null) 'visibility': visibility, + }); + } + + i1.RemoteAssetEntityCompanion copyWith( + {i0.Value? name, + i0.Value? type, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? durationInSeconds, + i0.Value? id, + i0.Value? checksum, + i0.Value? isFavorite, + i0.Value? ownerId, + i0.Value? localDateTime, + i0.Value? thumbHash, + i0.Value? deletedAt, + i0.Value? visibility}) { + return i1.RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + visibility: visibility ?? this.visibility, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertertype.toSql(type.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (checksum.present) { + map['checksum'] = i0.Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = i0.Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = i0.Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = i0.Variable(deletedAt.value); + } + if (visibility.present) { + map['visibility'] = i0.Variable(i1 + .$RemoteAssetEntityTable.$convertervisibility + .toSql(visibility.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('visibility: $visibility') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 955b2267d1f..b0c1e6e866d 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -78,7 +78,7 @@ class User { class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); - BlobColumn get id => blob()(); + TextColumn get id => text()(); TextColumn get name => text()(); BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); TextColumn get email => text()(); diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart index 474746a7921..32be9695182 100644 --- a/mobile/lib/infrastructure/entities/user.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user.entity.drift.dart @@ -3,13 +3,12 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3; -import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion Function({ - required i2.Uint8List id, + required String id, required String name, i0.Value isAdmin, required String email, @@ -20,7 +19,7 @@ typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion }); typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion Function({ - i0.Value id, + i0.Value id, i0.Value name, i0.Value isAdmin, i0.Value email, @@ -39,7 +38,7 @@ class $$UserEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnFilters get id => $composableBuilder( + i0.ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => i0.ColumnFilters(column)); i0.ColumnFilters get name => $composableBuilder( @@ -76,7 +75,7 @@ class $$UserEntityTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnOrderings get id => $composableBuilder( + i0.ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => i0.ColumnOrderings(column)); i0.ColumnOrderings get name => $composableBuilder( @@ -114,7 +113,7 @@ class $$UserEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumn get id => + i0.GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); i0.GeneratedColumn get name => @@ -167,7 +166,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager< createComputedFieldComposer: () => i1.$$UserEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - i0.Value id = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), i0.Value name = const i0.Value.absent(), i0.Value isAdmin = const i0.Value.absent(), i0.Value email = const i0.Value.absent(), @@ -187,7 +186,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager< quotaUsageInBytes: quotaUsageInBytes, ), createCompanionCallback: ({ - required i2.Uint8List id, + required String id, required String name, i0.Value isAdmin = const i0.Value.absent(), required String email, @@ -230,7 +229,7 @@ typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager< i1.UserEntityData, i0.PrefetchHooks Function()>; -class $UserEntityTable extends i3.UserEntity +class $UserEntityTable extends i2.UserEntity with i0.TableInfo<$UserEntityTable, i1.UserEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -238,9 +237,9 @@ class $UserEntityTable extends i3.UserEntity $UserEntityTable(this.attachedDatabase, [this._alias]); static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); @override - late final i0.GeneratedColumn id = - i0.GeneratedColumn('id', aliasedName, false, - type: i0.DriftSqlType.blob, requiredDuringInsert: true); + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); static const i0.VerificationMeta _nameMeta = const i0.VerificationMeta('name'); @override @@ -256,7 +255,7 @@ class $UserEntityTable extends i3.UserEntity requiredDuringInsert: false, defaultConstraints: i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), - defaultValue: const i4.Constant(false)); + defaultValue: const i3.Constant(false)); static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta('email'); @override @@ -276,7 +275,7 @@ class $UserEntityTable extends i3.UserEntity i0.GeneratedColumn('updated_at', aliasedName, false, type: i0.DriftSqlType.dateTime, requiredDuringInsert: false, - defaultValue: i4.currentDateAndTime); + defaultValue: i3.currentDateAndTime); static const i0.VerificationMeta _quotaSizeInBytesMeta = const i0.VerificationMeta('quotaSizeInBytes'); @override @@ -290,7 +289,7 @@ class $UserEntityTable extends i3.UserEntity i0.GeneratedColumn('quota_usage_in_bytes', aliasedName, false, type: i0.DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const i4.Constant(0)); + defaultValue: const i3.Constant(0)); @override List get $columns => [ id, @@ -366,7 +365,7 @@ class $UserEntityTable extends i3.UserEntity final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.UserEntityData( id: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!, + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, name: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, isAdmin: attachedDatabase.typeMapping @@ -397,7 +396,7 @@ class $UserEntityTable extends i3.UserEntity class UserEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List id; + final String id; final String name; final bool isAdmin; final String email; @@ -417,7 +416,7 @@ class UserEntityData extends i0.DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = i0.Variable(id); + map['id'] = i0.Variable(id); map['name'] = i0.Variable(name); map['is_admin'] = i0.Variable(isAdmin); map['email'] = i0.Variable(email); @@ -436,7 +435,7 @@ class UserEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return UserEntityData( - id: serializer.fromJson(json['id']), + id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), isAdmin: serializer.fromJson(json['isAdmin']), email: serializer.fromJson(json['email']), @@ -450,7 +449,7 @@ class UserEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), + 'id': serializer.toJson(id), 'name': serializer.toJson(name), 'isAdmin': serializer.toJson(isAdmin), 'email': serializer.toJson(email), @@ -462,7 +461,7 @@ class UserEntityData extends i0.DataClass } i1.UserEntityData copyWith( - {i2.Uint8List? id, + {String? id, String? name, bool? isAdmin, String? email, @@ -519,13 +518,13 @@ class UserEntityData extends i0.DataClass } @override - int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin, - email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath, + updatedAt, quotaSizeInBytes, quotaUsageInBytes); @override bool operator ==(Object other) => identical(this, other) || (other is i1.UserEntityData && - i0.$driftBlobEquality.equals(other.id, this.id) && + other.id == this.id && other.name == this.name && other.isAdmin == this.isAdmin && other.email == this.email && @@ -536,7 +535,7 @@ class UserEntityData extends i0.DataClass } class UserEntityCompanion extends i0.UpdateCompanion { - final i0.Value id; + final i0.Value id; final i0.Value name; final i0.Value isAdmin; final i0.Value email; @@ -555,7 +554,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { this.quotaUsageInBytes = const i0.Value.absent(), }); UserEntityCompanion.insert({ - required i2.Uint8List id, + required String id, required String name, this.isAdmin = const i0.Value.absent(), required String email, @@ -567,7 +566,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { name = i0.Value(name), email = i0.Value(email); static i0.Insertable custom({ - i0.Expression? id, + i0.Expression? id, i0.Expression? name, i0.Expression? isAdmin, i0.Expression? email, @@ -589,7 +588,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { } i1.UserEntityCompanion copyWith( - {i0.Value? id, + {i0.Value? id, i0.Value? name, i0.Value? isAdmin, i0.Value? email, @@ -613,7 +612,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (id.present) { - map['id'] = i0.Variable(id.value); + map['id'] = i0.Variable(id.value); } if (name.present) { map['name'] = i0.Variable(name.value); diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index ebbfeebadd0..302a9ffce19 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class UserMetadataEntity extends Table with DriftDefaultsMixin { const UserMetadataEntity(); - BlobColumn get userId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get userId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); TextColumn get preferences => text().map(userPreferenceConverter)(); @override diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart index 9829fd1acc0..95ab63ebf69 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart @@ -3,23 +3,22 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; -import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3; +import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart' - as i4; + as i3; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' - as i5; -import 'package:drift/internal/modular.dart' as i6; + as i4; +import 'package:drift/internal/modular.dart' as i5; typedef $$UserMetadataEntityTableCreateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }); typedef $$UserMetadataEntityTableUpdateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ - i0.Value userId, - i0.Value preferences, + i0.Value userId, + i0.Value preferences, }); final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< @@ -29,26 +28,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< $$UserMetadataEntityTableReferences( super.$_db, super.$_table, super.$_typedResult); - static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet( 'user_metadata_entity') .userId, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get userId { - final $_column = $_itemColumn('user_id')!; + i4.$$UserEntityTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_userIdTable($_db)); if (item == null) return manager; @@ -66,26 +65,26 @@ class $$UserMetadataEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnWithTypeConverterFilters get preferences => $composableBuilder( column: $table.preferences, builder: (column) => i0.ColumnWithTypeConverterFilters(column)); - i5.$$UserEntityTableFilterComposer get userId { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get userId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -108,20 +107,20 @@ class $$UserMetadataEntityTableOrderingComposer column: $table.preferences, builder: (column) => i0.ColumnOrderings(column)); - i5.$$UserEntityTableOrderingComposer get userId { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get userId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -140,24 +139,24 @@ class $$UserMetadataEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumnWithTypeConverter + i0.GeneratedColumnWithTypeConverter get preferences => $composableBuilder( column: $table.preferences, builder: (column) => column); - i5.$$UserEntityTableAnnotationComposer get userId { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get userId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -193,16 +192,16 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager< i1.$$UserMetadataEntityTableAnnotationComposer( $db: db, $table: table), updateCompanionCallback: ({ - i0.Value userId = const i0.Value.absent(), - i0.Value preferences = const i0.Value.absent(), + i0.Value userId = const i0.Value.absent(), + i0.Value preferences = const i0.Value.absent(), }) => i1.UserMetadataEntityCompanion( userId: userId, preferences: preferences, ), createCompanionCallback: ({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }) => i1.UserMetadataEntityCompanion.insert( userId: userId, @@ -267,7 +266,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager i1.UserMetadataEntityData, i0.PrefetchHooks Function({bool userId})>; -class $UserMetadataEntityTable extends i4.UserMetadataEntity +class $UserMetadataEntityTable extends i3.UserMetadataEntity with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -276,18 +275,18 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity static const i0.VerificationMeta _userIdMeta = const i0.VerificationMeta('userId'); @override - late final i0.GeneratedColumn userId = - i0.GeneratedColumn('user_id', aliasedName, false, - type: i0.DriftSqlType.blob, - requiredDuringInsert: true, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final i0.GeneratedColumn userId = i0.GeneratedColumn( + 'user_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); @override - late final i0.GeneratedColumnWithTypeConverter + late final i0.GeneratedColumnWithTypeConverter preferences = i0.GeneratedColumn( 'preferences', aliasedName, false, type: i0.DriftSqlType.string, requiredDuringInsert: true) - .withConverter( + .withConverter( i1.$UserMetadataEntityTable.$converterpreferences); @override List get $columns => [userId, preferences]; @@ -319,7 +318,7 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.UserMetadataEntityData( userId: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!, + .read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!, preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql( attachedDatabase.typeMapping.read( i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!), @@ -331,8 +330,8 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity return $UserMetadataEntityTable(attachedDatabase, alias); } - static i0.JsonTypeConverter2 - $converterpreferences = i4.userPreferenceConverter; + static i0.JsonTypeConverter2 + $converterpreferences = i3.userPreferenceConverter; @override bool get withoutRowId => true; @override @@ -341,14 +340,14 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity class UserMetadataEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List userId; - final i3.UserPreferences preferences; + final String userId; + final i2.UserPreferences preferences; const UserMetadataEntityData( {required this.userId, required this.preferences}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['user_id'] = i0.Variable(userId); + map['user_id'] = i0.Variable(userId); { map['preferences'] = i0.Variable( i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences)); @@ -360,7 +359,7 @@ class UserMetadataEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return UserMetadataEntityData( - userId: serializer.fromJson(json['userId']), + userId: serializer.fromJson(json['userId']), preferences: i1.$UserMetadataEntityTable.$converterpreferences .fromJson(serializer.fromJson(json['preferences'])), ); @@ -369,7 +368,7 @@ class UserMetadataEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), + 'userId': serializer.toJson(userId), 'preferences': serializer.toJson(i1 .$UserMetadataEntityTable.$converterpreferences .toJson(preferences)), @@ -377,7 +376,7 @@ class UserMetadataEntityData extends i0.DataClass } i1.UserMetadataEntityData copyWith( - {i2.Uint8List? userId, i3.UserPreferences? preferences}) => + {String? userId, i2.UserPreferences? preferences}) => i1.UserMetadataEntityData( userId: userId ?? this.userId, preferences: preferences ?? this.preferences, @@ -401,31 +400,30 @@ class UserMetadataEntityData extends i0.DataClass } @override - int get hashCode => - Object.hash(i0.$driftBlobEquality.hash(userId), preferences); + int get hashCode => Object.hash(userId, preferences); @override bool operator ==(Object other) => identical(this, other) || (other is i1.UserMetadataEntityData && - i0.$driftBlobEquality.equals(other.userId, this.userId) && + other.userId == this.userId && other.preferences == this.preferences); } class UserMetadataEntityCompanion extends i0.UpdateCompanion { - final i0.Value userId; - final i0.Value preferences; + final i0.Value userId; + final i0.Value preferences; const UserMetadataEntityCompanion({ this.userId = const i0.Value.absent(), this.preferences = const i0.Value.absent(), }); UserMetadataEntityCompanion.insert({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }) : userId = i0.Value(userId), preferences = i0.Value(preferences); static i0.Insertable custom({ - i0.Expression? userId, + i0.Expression? userId, i0.Expression? preferences, }) { return i0.RawValuesInsertable({ @@ -435,8 +433,7 @@ class UserMetadataEntityCompanion } i1.UserMetadataEntityCompanion copyWith( - {i0.Value? userId, - i0.Value? preferences}) { + {i0.Value? userId, i0.Value? preferences}) { return i1.UserMetadataEntityCompanion( userId: userId ?? this.userId, preferences: preferences ?? this.preferences, @@ -447,7 +444,7 @@ class UserMetadataEntityCompanion Map toColumns(bool nullToAbsent) { final map = {}; if (userId.present) { - map['user_id'] = i0.Variable(userId.value); + map['user_id'] = i0.Variable(userId.value); } if (preferences.present) { map['preferences'] = i0.Variable(i1 diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 997714e1b6b..4ad60276a22 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,7 +3,12 @@ import 'dart:async'; 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/exif.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:isar/isar.dart'; @@ -25,7 +30,18 @@ class IsarDatabaseRepository implements IDatabaseRepository { Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); } -@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity]) +@DriftDatabase( + tables: [ + UserEntity, + UserMetadataEntity, + PartnerEntity, + LocalAlbumEntity, + LocalAssetEntity, + LocalAlbumAssetEntity, + RemoteAssetEntity, + RemoteExifEntity, + ], +) class Drift extends $Drift implements IDatabaseRepository { Drift([QueryExecutor? executor]) : super( @@ -42,8 +58,9 @@ class Drift extends $Drift implements IDatabaseRepository { @override MigrationStrategy get migration => MigrationStrategy( beforeOpen: (details) async { - await customStatement('PRAGMA journal_mode = WAL'); await customStatement('PRAGMA foreign_keys = ON'); + await customStatement('PRAGMA synchronous = NORMAL'); + await customStatement('PRAGMA journal_mode = WAL'); }, ); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index a4c2b31dcd3..d1bda93653f 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -7,6 +7,16 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift as i2; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i4; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i5; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i6; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i7; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' + as i8; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -16,12 +26,32 @@ abstract class $Drift extends i0.GeneratedDatabase { i2.$UserMetadataEntityTable(this); late final i3.$PartnerEntityTable partnerEntity = i3.$PartnerEntityTable(this); + late final i4.$LocalAlbumEntityTable localAlbumEntity = + i4.$LocalAlbumEntityTable(this); + late final i5.$LocalAssetEntityTable localAssetEntity = + i5.$LocalAssetEntityTable(this); + late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = + i6.$LocalAlbumAssetEntityTable(this); + late final i7.$RemoteAssetEntityTable remoteAssetEntity = + i7.$RemoteAssetEntityTable(this); + late final i8.$RemoteExifEntityTable remoteExifEntity = + i8.$RemoteExifEntityTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [userEntity, userMetadataEntity, partnerEntity]; + List get allSchemaEntities => [ + userEntity, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAssetEntity, + localAlbumAssetEntity, + remoteAssetEntity, + remoteExifEntity, + i5.idxLocalAssetChecksum, + i7.uQRemoteAssetOwnerChecksum + ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => const i0.StreamQueryUpdateRules( @@ -48,6 +78,36 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete), + ], + ), ], ); @override @@ -64,4 +124,14 @@ class $DriftManager { i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i3.$$PartnerEntityTableTableManager get partnerEntity => i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); + i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity => + i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); + i5.$$LocalAssetEntityTableTableManager get localAssetEntity => + i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); + i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 + .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); + i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity => + i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); + i8.$$RemoteExifEntityTableTableManager get remoteExifEntity => + i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); } diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart new file mode 100644 index 00000000000..355487b193a --- /dev/null +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -0,0 +1,382 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:platform/platform.dart'; + +class DriftLocalAlbumRepository extends DriftDatabaseRepository + implements ILocalAlbumRepository { + final Drift _db; + final Platform _platform; + const DriftLocalAlbumRepository(this._db, {Platform? platform}) + : _platform = platform ?? const LocalPlatform(), + super(_db); + + @override + Future> getAll({SortLocalAlbumsBy? sortBy}) { + final assetCount = _db.localAlbumAssetEntity.assetId.count(); + + final query = _db.localAlbumEntity.select().join([ + leftOuterJoin( + _db.localAlbumAssetEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]); + query + ..addColumns([assetCount]) + ..groupBy([_db.localAlbumEntity.id]); + if (sortBy == SortLocalAlbumsBy.id) { + query.orderBy([OrderingTerm.asc(_db.localAlbumEntity.id)]); + } + return query + .map( + (row) => row + .readTable(_db.localAlbumEntity) + .toDto(assetCount: row.read(assetCount) ?? 0), + ) + .get(); + } + + @override + Future delete(String albumId) => transaction(() async { + // Remove all assets that are only in this particular album + // We cannot remove all assets in the album because they might be in other albums in iOS + // That is not the case on Android since asset <-> album has one:one mapping + final assetsToDelete = _platform.isIOS + ? await _getUniqueAssetsInAlbum(albumId) + : await getAssetIdsForAlbum(albumId); + await _deleteAssets(assetsToDelete); + + // All the other assets that are still associated will be unlinked automatically on-cascade + await _db.managers.localAlbumEntity + .filter((a) => a.id.equals(albumId)) + .delete(); + }); + + @override + Future syncAlbumDeletes( + String albumId, + Iterable assetIdsToKeep, + ) async { + if (assetIdsToKeep.isEmpty) { + return Future.value(); + } + + final deleteSmt = _db.localAssetEntity.delete(); + deleteSmt.where((localAsset) { + final subQuery = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId + .equalsExp(_db.localAlbumEntity.id), + ), + ]); + subQuery.where( + _db.localAlbumEntity.id.equals(albumId) & + _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep), + ); + return localAsset.id.isInQuery(subQuery); + }); + await deleteSmt.go(); + } + + @override + Future upsert( + LocalAlbum localAlbum, { + Iterable toUpsert = const [], + Iterable toDelete = const [], + }) { + final companion = LocalAlbumEntityCompanion.insert( + id: localAlbum.id, + name: localAlbum.name, + updatedAt: Value(localAlbum.updatedAt), + backupSelection: localAlbum.backupSelection, + ); + + return _db.transaction(() async { + await _db.localAlbumEntity + .insertOne(companion, onConflict: DoUpdate((_) => companion)); + await _addAssets(localAlbum.id, toUpsert); + await _removeAssets(localAlbum.id, toDelete); + }); + } + + @override + Future updateAll(Iterable albums) { + return _db.transaction(() async { + await _db.localAlbumEntity + .update() + .write(const LocalAlbumEntityCompanion(marker_: Value(true))); + + await _db.batch((batch) { + for (final album in albums) { + final companion = LocalAlbumEntityCompanion.insert( + id: album.id, + name: album.name, + updatedAt: Value(album.updatedAt), + backupSelection: album.backupSelection, + marker_: const Value(null), + ); + + batch.insert( + _db.localAlbumEntity, + companion, + onConflict: DoUpdate((_) => companion), + ); + } + }); + + if (_platform.isAndroid) { + // On Android, an asset can only be in one album + // So, get the albums that are marked for deletion + // and delete all the assets that are in those albums + final deleteSmt = _db.localAssetEntity.delete(); + deleteSmt.where((localAsset) { + final subQuery = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId + .equalsExp(_db.localAlbumEntity.id), + ), + ]); + subQuery.where(_db.localAlbumEntity.marker_.isNotNull()); + return localAsset.id.isInQuery(subQuery); + }); + await deleteSmt.go(); + } + + await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull()); + }); + } + + @override + Future> getAssetsForAlbum(String albumId) { + final query = _db.localAlbumAssetEntity.select().join( + [ + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + ), + ], + ) + ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) + ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto()) + .get(); + } + + @override + Future> getAssetIdsForAlbum(String albumId) { + final query = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)); + return query + .map((row) => row.read(_db.localAlbumAssetEntity.assetId)!) + .get(); + } + + @override + Future processDelta({ + required List updates, + required List deletes, + required Map> assetAlbums, + }) { + return _db.transaction(() async { + await _deleteAssets(deletes); + + await _upsertAssets(updates); + // The ugly casting below is required for now because the generated code + // casts the returned values from the platform during decoding them + // and iterating over them causes the type to be List instead of + // List + await _db.batch((batch) async { + assetAlbums.cast>().forEach((assetId, albumIds) { + batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => + f.albumId.isNotIn(albumIds.cast().nonNulls) & + f.assetId.equals(assetId), + ); + }); + }); + await _db.batch((batch) async { + assetAlbums.cast>().forEach((assetId, albumIds) { + batch.insertAll( + _db.localAlbumAssetEntity, + albumIds.cast().nonNulls.map( + (albumId) => LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + ), + onConflict: DoNothing(), + ); + }); + }); + }); + } + + Future _addAssets(String albumId, Iterable assets) { + if (assets.isEmpty) { + return Future.value(); + } + return transaction(() async { + await _upsertAssets(assets); + await _db.localAlbumAssetEntity.insertAll( + assets.map( + (a) => LocalAlbumAssetEntityCompanion.insert( + assetId: a.id, + albumId: albumId, + ), + ), + mode: InsertMode.insertOrIgnore, + ); + }); + } + + Future _removeAssets(String albumId, Iterable assetIds) async { + if (assetIds.isEmpty) { + return Future.value(); + } + + if (_platform.isAndroid) { + return _deleteAssets(assetIds); + } + + List assetsToDelete = []; + List assetsToUnLink = []; + + final uniqueAssets = await _getUniqueAssetsInAlbum(albumId); + if (uniqueAssets.isEmpty) { + assetsToUnLink = assetIds.toList(); + } else { + // Delete unique assets and unlink others + final uniqueSet = uniqueAssets.toSet(); + + for (final assetId in assetIds) { + if (uniqueSet.contains(assetId)) { + assetsToDelete.add(assetId); + } else { + assetsToUnLink.add(assetId); + } + } + } + + return transaction(() async { + if (assetsToUnLink.isNotEmpty) { + await _db.batch( + (batch) => batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId), + ), + ); + } + + await _deleteAssets(assetsToDelete); + }); + } + + /// Get all asset ids that are only in this album and not in other albums. + /// This is useful in cases where the album is a smart album or a user-created album, especially on iOS + Future> _getUniqueAssetsInAlbum(String albumId) { + final assetId = _db.localAlbumAssetEntity.assetId; + final query = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([assetId]) + ..groupBy( + [assetId], + having: _db.localAlbumAssetEntity.albumId.count().equals(1) & + _db.localAlbumAssetEntity.albumId.equals(albumId), + ); + + return query.map((row) => row.read(assetId)!).get(); + } + + Future _upsertAssets(Iterable localAssets) { + if (localAssets.isEmpty) { + return Future.value(); + } + + return _db.batch((batch) async { + batch.insertAllOnConflictUpdate( + _db.localAssetEntity, + localAssets.map( + (a) => LocalAssetEntityCompanion.insert( + name: a.name, + type: a.type, + createdAt: Value(a.createdAt), + updatedAt: Value(a.updatedAt), + durationInSeconds: Value.absentIfNull(a.durationInSeconds), + id: a.id, + checksum: Value.absentIfNull(a.checksum), + ), + ), + ); + }); + } + + Future _deleteAssets(Iterable ids) { + if (ids.isEmpty) { + return Future.value(); + } + + return _db.batch( + (batch) => batch.deleteWhere( + _db.localAssetEntity, + (f) => f.id.isIn(ids), + ), + ); + } + + @override + Future update(LocalAlbum album) { + return (_db.update(_db.localAlbumEntity) + ..where( + (a) => a.id.equals(album.id), + )) + .write( + LocalAlbumEntityCompanion.insert( + id: album.id, + name: album.name, + updatedAt: Value(album.updatedAt), + backupSelection: album.backupSelection, + ), + ); + } +} + +extension on LocalAlbumEntityData { + LocalAlbum toDto({int assetCount = 0}) { + return LocalAlbum( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + backupSelection: backupSelection, + ); + } +} + +extension on LocalAssetEntityData { + LocalAsset toDto() { + return LocalAsset( + id: id, + name: name, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + isFavorite: isFavorite, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index c69122335ec..2349f35df72 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -105,6 +106,7 @@ class SyncApiRepository implements ISyncApiRepository { stopwatch.stop(); _logger .info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); + DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); } List _parseLines(List lines) { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 5ad9a369df6..804f66c5bbc 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,12 +1,13 @@ import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; +import 'package:openapi/api.dart' hide AssetVisibility; class DriftSyncStreamRepository extends DriftDatabaseRepository implements ISyncStreamRepository { @@ -22,7 +23,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository for (final user in data) { batch.delete( _db.userEntity, - UserEntityCompanion(id: Value(user.userId.toUuidByte())), + UserEntityCompanion(id: Value(user.userId)), ); } }); @@ -44,7 +45,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.userEntity, - companion.copyWith(id: Value(user.id.toUuidByte())), + companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion), ); } @@ -63,8 +64,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.delete( _db.partnerEntity, PartnerEntityCompanion( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), ); } @@ -86,8 +87,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.partnerEntity, companion.copyWith( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), onConflict: DoUpdate((_) => companion), ); @@ -99,36 +100,153 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository } } - // Assets - @override - Future updateAssetsV1(Iterable data) async { - debugPrint("updateAssetsV1 - ${data.length}"); - } - @override Future deleteAssetsV1(Iterable data) async { - debugPrint("deleteAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deleteAssetsV1', e, s); + rethrow; + } } - // Partner Assets @override - Future updatePartnerAssetsV1(Iterable data) async { - debugPrint("updatePartnerAssetsV1 - ${data.length}"); + Future updateAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsV1', e, s); + rethrow; + } } @override Future deletePartnerAssetsV1(Iterable data) async { - debugPrint("deletePartnerAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deletePartnerAssetsV1', e, s); + rethrow; + } + } + + @override + Future updatePartnerAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsV1', e, s); + rethrow; + } } - // EXIF @override Future updateAssetsExifV1(Iterable data) async { - debugPrint("updateAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsExifV1', e, s); + rethrow; + } } @override Future updatePartnerAssetsExifV1(Iterable data) async { - debugPrint("updatePartnerAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsExifV1', e, s); + rethrow; + } } + + Future _updateAssetsV1(Iterable data) => + _db.batch((batch) { + for (final asset in data) { + final companion = RemoteAssetEntityCompanion( + name: Value(asset.originalFileName), + type: Value(asset.type.toAssetType()), + createdAt: Value.absentIfNull(asset.fileCreatedAt), + updatedAt: Value.absentIfNull(asset.fileModifiedAt), + durationInSeconds: const Value(0), + checksum: Value(asset.checksum), + isFavorite: Value(asset.isFavorite), + ownerId: Value(asset.ownerId), + localDateTime: Value(asset.localDateTime), + thumbHash: Value(asset.thumbhash), + deletedAt: Value(asset.deletedAt), + visibility: Value(asset.visibility.toAssetVisibility()), + ); + + batch.insert( + _db.remoteAssetEntity, + companion.copyWith(id: Value(asset.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + + Future _deleteAssetsV1(Iterable assets) => + _db.batch((batch) { + for (final asset in assets) { + batch.delete( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion(id: Value(asset.assetId)), + ); + } + }); + + Future _updateAssetExifV1(Iterable data) => + _db.batch((batch) { + for (final exif in data) { + final companion = RemoteExifEntityCompanion( + city: Value(exif.city), + state: Value(exif.state), + country: Value(exif.country), + dateTimeOriginal: Value(exif.dateTimeOriginal), + description: Value(exif.description), + height: Value(exif.exifImageHeight), + width: Value(exif.exifImageWidth), + exposureTime: Value(exif.exposureTime), + fNumber: Value(exif.fNumber), + fileSize: Value(exif.fileSizeInByte), + focalLength: Value(exif.focalLength), + latitude: Value(exif.latitude), + longitude: Value(exif.longitude), + iso: Value(exif.iso), + make: Value(exif.make), + model: Value(exif.model), + orientation: Value(exif.orientation), + timeZone: Value(exif.timeZone), + rating: Value(exif.rating), + projectionType: Value(exif.projectionType), + ); + + batch.insert( + _db.remoteExifEntity, + companion.copyWith(assetId: Value(exif.assetId)), + onConflict: DoUpdate((_) => companion), + ); + } + }); +} + +extension on SyncAssetV1TypeEnum { + AssetType toAssetType() => switch (this) { + SyncAssetV1TypeEnum.IMAGE => AssetType.image, + SyncAssetV1TypeEnum.VIDEO => AssetType.video, + SyncAssetV1TypeEnum.AUDIO => AssetType.audio, + SyncAssetV1TypeEnum.OTHER => AssetType.other, + _ => throw Exception('Unknown SyncAssetV1TypeEnum value: $this'), + }; +} + +extension on SyncAssetV1VisibilityEnum { + AssetVisibility toAssetVisibility() => switch (this) { + SyncAssetV1VisibilityEnum.timeline => AssetVisibility.timeline, + SyncAssetV1VisibilityEnum.hidden => AssetVisibility.hidden, + SyncAssetV1VisibilityEnum.archive => AssetVisibility.archive, + SyncAssetV1VisibilityEnum.locked => AssetVisibility.locked, + _ => throw Exception('Unknown SyncAssetV1VisibilityEnum value: $this'), + }; } diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart new file mode 100644 index 00000000000..86495508268 --- /dev/null +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; + +mixin AssetEntityMixin on Table { + TextColumn get name => text()(); + IntColumn get type => intEnum()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get durationInSeconds => integer().nullable()(); +} diff --git a/mobile/lib/interfaces/timeline.interface.dart b/mobile/lib/interfaces/timeline.interface.dart index 3a4cce3cb66..ecdc0dcf0c6 100644 --- a/mobile/lib/interfaces/timeline.interface.dart +++ b/mobile/lib/interfaces/timeline.interface.dart @@ -14,7 +14,7 @@ abstract class ITimelineRepository { Album album, GroupAssetsBy groupAssetsBy, ); - Stream watchAllVideosTimeline(); + Stream watchAllVideosTimeline(String userId); Stream watchHomeTimeline( String userId, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 3c7c1fbe4d5..32bb025916d 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -89,18 +89,6 @@ Future initApp() async { initializeTimeZones(); - FileDownloader().configureNotification( - running: TaskNotification( - 'downloading_media'.tr(), - 'file: {filename}', - ), - complete: TaskNotification( - 'download_finished'.tr(), - 'file: {filename}', - ), - progressBar: true, - ); - await FileDownloader().trackTasksInGroup( downloadGroupLivePhoto, markDownloadedComplete: false, @@ -167,10 +155,27 @@ class ImmichAppState extends ConsumerState await ref.read(localNotificationService).setup(); } + void _configureFileDownloaderNotifications() { + FileDownloader().configureNotification( + running: TaskNotification( + 'downloading_media'.tr(), + '${'file_name'.tr()}: {filename}', + ), + complete: TaskNotification( + 'download_finished'.tr(), + '${'file_name'.tr()}: {filename}', + ), + progressBar: true, + ); + } + @override void didChangeDependencies() { super.didChangeDependencies(); Intl.defaultLocale = context.locale.toLanguageTag(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _configureFileDownloaderNotifications(); + }); } @override diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart index 75b2c09af3b..f6c46843db2 100644 --- a/mobile/lib/pages/album/album_viewer.dart +++ b/mobile/lib/pages/album/album_viewer.dart @@ -14,6 +14,7 @@ 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'; @@ -93,6 +94,7 @@ class AlbumViewer extends HookConsumerWidget { onActivitiesPressed() { if (album.remoteId != null) { + ref.read(currentAssetProvider.notifier).set(null); context.pushRoute( const ActivitiesRoute(), ); diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart index 8cc09a1ded3..9d8ebb7673b 100644 --- a/mobile/lib/pages/albums/albums.page.dart +++ b/mobile/lib/pages/albums/albums.page.dart @@ -14,6 +14,7 @@ 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/utils/translation.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'; @@ -229,13 +230,11 @@ class AlbumsPage extends HookConsumerWidget { ), subtitle: sorted[index].ownerId != null ? Text( - '${(sorted[index].assetCount == 1 ? 'album_thumbnail_card_item'.tr() : 'album_thumbnail_card_items'.tr( - namedArgs: { - 'count': sorted[index] - .assetCount - .toString(), - }, - ))} • ${sorted[index].ownerId != userId ? 'album_thumbnail_shared_by'.tr(namedArgs: {'user': sorted[index].ownerName!}) : 'owned'.tr()}', + '${t('items_count', { + 'count': sorted[index].assetCount, + })} • ${sorted[index].ownerId != userId ? t('shared_by_user', { + 'user': sorted[index].ownerName!, + }) : 'owned'.tr()}', overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index c4124efb525..5082e30608b 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -3,9 +3,11 @@ 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/local_album.model.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/providers/backup/backup_album.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'; @@ -17,12 +19,17 @@ 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 albums = ref.watch(backupAlbumProvider); + + final selectedBackupAlbums = albums + .where((album) => album.backupSelection == BackupSelection.selected) + .toList(); + final excludedBackupAlbums = albums + .where((album) => album.backupSelection == BackupSelection.excluded) + .toList(); final enableSyncUploadAlbum = useAppSettingsState(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; - final albums = ref.watch(backupProvider).availableAlbums; useEffect( () { @@ -85,8 +92,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { buildSelectedAlbumNameChip() { return selectedBackupAlbums.map((album) { - void removeSelection() => - ref.read(backupProvider.notifier).removeAlbumForBackup(album); + void removeSelection() { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } return Padding( padding: const EdgeInsets.only(right: 8.0), @@ -117,9 +125,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { buildExcludedAlbumNameChip() { return excludedBackupAlbums.map((album) { void removeSelection() { - ref - .watch(backupProvider.notifier) - .removeExcludedAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } return GestureDetector( @@ -168,129 +174,131 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ).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(), - }, + body: SafeArea( + child: CustomScrollView( + physics: const ClampingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, ), - 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, - ), + "backup_album_selection_page_selection_info", + style: context.textTheme.titleSmall, ).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: RoundedRectangleBorder( - borderRadius: BorderRadius.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(), - ], - ), - ), - ); - }, - ); - }, - ), - ), + // Selected Album Chips - // buildSearchBar(), - ], + 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: RoundedRectangleBorder( + borderRadius: BorderRadius.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(); - } - }, - ), - ], + 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 index 18655a0be99..20407f397db 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -6,11 +6,13 @@ 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/local_album.model.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/backup_album.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'; @@ -86,8 +88,13 @@ class BackupControllerPage extends HookConsumerWidget { ); Widget buildSelectedAlbumName() { - var text = "backup_controller_page_backup_selected".tr(); - var albums = ref.watch(backupProvider).selectedBackupAlbums; + String text = "backup_controller_page_backup_selected".tr(); + final albums = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.selected, + ) + .toList(); if (albums.isNotEmpty) { for (var album in albums) { @@ -121,8 +128,13 @@ class BackupControllerPage extends HookConsumerWidget { } Widget buildExcludedAlbumName() { - var text = "backup_controller_page_excluded".tr(); - var albums = ref.watch(backupProvider).excludedBackupAlbums; + String text = "backup_controller_page_excluded".tr(); + final albums = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.excluded, + ) + .toList(); if (albums.isNotEmpty) { for (var album in albums) { diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 420b699730e..bdde338cb36 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -223,7 +223,8 @@ class GalleryViewerPage extends HookConsumerWidget { heroAttributes: _getHeroAttributes(asset), filterQuality: FilterQuality.high, tightMode: true, - minScale: PhotoViewComputedScale.contained, + initialScale: PhotoViewComputedScale.contained * 0.99, + minScale: PhotoViewComputedScale.contained * 0.99, errorBuilder: (context, error, stackTrace) => ImmichImage( asset, fit: BoxFit.contain, @@ -238,9 +239,9 @@ class GalleryViewerPage extends HookConsumerWidget { onDragUpdate: (_, details, __) => handleSwipeUpDown(details), heroAttributes: _getHeroAttributes(asset), filterQuality: FilterQuality.high, - initialScale: 1.0, + initialScale: PhotoViewComputedScale.contained * 0.99, maxScale: 1.0, - minScale: 1.0, + minScale: PhotoViewComputedScale.contained * 0.99, basePosition: Alignment.center, child: SizedBox( width: context.width, diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index dc186720f31..05c76069708 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -30,7 +30,7 @@ enum SettingSection { "backup_setting_subtitle", ), languages( - 'setting_languages_title', + 'language', Icons.language, "setting_languages_subtitle", ), diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index af6f295970c..6ac7d60f9b4 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -9,6 +9,7 @@ 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/providers/folder.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; @@ -219,12 +220,15 @@ class FolderContent extends HookConsumerWidget { list.allAssets!.isNotEmpty) ...list.allAssets!.map( (asset) => LargeLeadingTile( - onTap: () => context.pushRoute( - GalleryViewerRoute( - renderList: list, - initialIndex: list.allAssets!.indexOf(asset), - ), - ), + onTap: () { + ref.read(currentAssetProvider.notifier).set(asset); + context.pushRoute( + GalleryViewerRoute( + renderList: list, + initialIndex: list.allAssets!.indexOf(asset), + ), + ); + }, leading: ClipRRect( borderRadius: const BorderRadius.all( Radius.circular(15), diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart index 27daf0a8879..6ec05485460 100644 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -133,7 +133,7 @@ class PeopleCollectionPage extends HookConsumerWidget { ); }, error: (error, stack) => const Text("error"), - loading: () => const CircularProgressIndicator(), + loading: () => const Center(child: CircularProgressIndicator()), ), ); }, diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index b80b96f94f1..022bf5da5f6 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -395,6 +395,7 @@ class _MapWithMarker extends StatelessWidget { children: [ style.widgetWhen( onData: (style) => MapLibreMap( + attributionButtonMargins: const Point(8, kToolbarHeight), initialCameraPosition: CameraPosition( target: initialLocation ?? const LatLng(0, 0), zoom: initialLocation != null ? 12 : 0, diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart new file mode 100644 index 00000000000..c4e4c467d41 --- /dev/null +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -0,0 +1,501 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && + a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + +class PlatformAsset { + PlatformAsset({ + required this.id, + required this.name, + required this.type, + this.createdAt, + this.updatedAt, + required this.durationInSeconds, + }); + + String id; + + String name; + + int type; + + int? createdAt; + + int? updatedAt; + + int durationInSeconds; + + List _toList() { + return [ + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformAsset decode(Object result) { + result as List; + return PlatformAsset( + id: result[0]! as String, + name: result[1]! as String, + type: result[2]! as int, + createdAt: result[3] as int?, + updatedAt: result[4] as int?, + durationInSeconds: result[5]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformAsset || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class PlatformAlbum { + PlatformAlbum({ + required this.id, + required this.name, + this.updatedAt, + required this.isCloud, + required this.assetCount, + }); + + String id; + + String name; + + int? updatedAt; + + bool isCloud; + + int assetCount; + + List _toList() { + return [ + id, + name, + updatedAt, + isCloud, + assetCount, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformAlbum decode(Object result) { + result as List; + return PlatformAlbum( + id: result[0]! as String, + name: result[1]! as String, + updatedAt: result[2] as int?, + isCloud: result[3]! as bool, + assetCount: result[4]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformAlbum || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class SyncDelta { + SyncDelta({ + required this.hasChanges, + required this.updates, + required this.deletes, + required this.assetAlbums, + }); + + bool hasChanges; + + List updates; + + List deletes; + + Map> assetAlbums; + + List _toList() { + return [ + hasChanges, + updates, + deletes, + assetAlbums, + ]; + } + + Object encode() { + return _toList(); + } + + static SyncDelta decode(Object result) { + result as List; + return SyncDelta( + hasChanges: result[0]! as bool, + updates: (result[1] as List?)!.cast(), + deletes: (result[2] as List?)!.cast(), + assetAlbums: + (result[3] as Map?)!.cast>(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SyncDelta || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PlatformAsset) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PlatformAlbum) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SyncDelta) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return PlatformAsset.decode(readValue(buffer)!); + case 130: + return PlatformAlbum.decode(readValue(buffer)!); + case 131: + return SyncDelta.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class NativeSyncApi { + /// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NativeSyncApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future shouldFullSync() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future getMediaChanges() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as SyncDelta?)!; + } + } + + Future checkpointSync() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future clearSyncCheckpoint() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future> getAssetIdsForAlbum(String albumId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future> getAlbums() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future getAssetsCountSince(String albumId, int timestamp) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId, timestamp]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as int?)!; + } + } + + Future> getAssetsForAlbum(String albumId, + {int? updatedTimeCond}) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId, updatedTimeCond]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } +} diff --git a/mobile/lib/presentation/pages/dev/dev_logger.dart b/mobile/lib/presentation/pages/dev/dev_logger.dart new file mode 100644 index 00000000000..6d179241a48 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/dev_logger.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; +// ignore: import_rule_isar +import 'package:isar/isar.dart'; + +const kDevLoggerTag = 'DEV'; + +abstract final class DLog { + const DLog(); + + static Stream> watchLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return const Stream.empty(); + } + + return db.loggerMessages + .filter() + .context1EqualTo(kDevLoggerTag) + .sortByCreatedAtDesc() + .watch(fireImmediately: true) + .map((logs) => logs.map((log) => log.toDto()).toList()); + } + + static void clearLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return; + } + + db.writeTxnSync(() { + db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync(); + }); + } + + static void log(String message, [Object? error, StackTrace? stackTrace]) { + debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message'); + if (error != null) { + debugPrint('Error: $error'); + } + if (stackTrace != null) { + debugPrint('StackTrace: $stackTrace'); + } + + final isar = Isar.getInstance(); + if (isar == null) { + debugPrint('Isar is not initialized'); + return; + } + + final record = LogMessage( + message: message, + level: LogLevel.info, + createdAt: DateTime.now(), + logger: kDevLoggerTag, + error: error?.toString(), + stack: stackTrace?.toString(), + ); + + unawaited(IsarLogRepository(isar).insert(record)); + } +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart new file mode 100644 index 00000000000..3ff0b12b959 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -0,0 +1,201 @@ +// ignore_for_file: avoid-local-functions + +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:drift/drift.dart' hide Column; +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/presentation/pages/dev/dev_logger.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +final _features = [ + _Feature( + name: 'Sync Local', + icon: Icons.photo_album_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(), + ), + _Feature( + name: 'Sync Local Full', + icon: Icons.photo_library_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true), + ), + _Feature( + name: 'Sync Remote', + icon: Icons.refresh_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(), + ), + _Feature( + name: 'WAL Checkpoint', + icon: Icons.save_rounded, + onTap: (_, ref) => ref + .read(driftProvider) + .customStatement("pragma wal_checkpoint(truncate)"), + ), + _Feature( + name: 'Clear Delta Checkpoint', + icon: Icons.delete_rounded, + onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(), + ), + _Feature( + name: 'Clear Local Data', + icon: Icons.delete_forever_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.localAssetEntity.deleteAll(); + await db.localAlbumEntity.deleteAll(); + await db.localAlbumAssetEntity.deleteAll(); + }, + ), + _Feature( + name: 'Clear Remote Data', + icon: Icons.delete_sweep_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.remoteAssetEntity.deleteAll(); + await db.remoteExifEntity.deleteAll(); + }, + ), + _Feature( + name: 'Local Media Summary', + icon: Icons.table_chart_rounded, + onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), + ), + _Feature( + name: 'Remote Media Summary', + icon: Icons.summarize_rounded, + onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()), + ), + _Feature( + name: 'Reset Sqlite', + icon: Icons.table_view_rounded, + onTap: (_, ref) async { + final drift = ref.read(driftProvider); + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + final migrator = drift.createMigrator(); + for (final entity in drift.allSchemaEntities) { + await migrator.drop(entity); + await migrator.create(entity); + } + }, + ), +]; + +@RoutePage() +class FeatInDevPage extends StatelessWidget { + const FeatInDevPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Features in Development'), + centerTitle: true, + ), + body: Column( + children: [ + Flexible( + flex: 1, + child: ListView.builder( + itemBuilder: (_, index) { + final feat = _features[index]; + return Consumer( + builder: (ctx, ref, _) => ListTile( + title: Text(feat.name), + trailing: Icon(feat.icon), + visualDensity: VisualDensity.compact, + onTap: () => unawaited(feat.onTap(ctx, ref)), + ), + ); + }, + itemCount: _features.length, + ), + ), + const Divider(height: 0), + const Flexible(child: _DevLogs()), + ], + ), + ); + } +} + +class _Feature { + const _Feature({ + required this.name, + required this.icon, + required this.onTap, + }); + + final String name; + final IconData icon; + final Future Function(BuildContext, WidgetRef _) onTap; +} + +// ignore: prefer-single-widget-per-file +class _DevLogs extends StatelessWidget { + const _DevLogs(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + IconButton( + onPressed: DLog.clearLog, + icon: Icon( + Icons.delete_outline_rounded, + size: 20.0, + color: context.primaryColor, + semanticLabel: "Clear logs", + ), + ), + ], + centerTitle: true, + ), + body: StreamBuilder( + initialData: [], + stream: DLog.watchLog(), + builder: (_, logMessages) { + return ListView.separated( + itemBuilder: (ctx, index) { + // ignore: avoid-unsafe-collection-methods + final logMessage = logMessages.data![index]; + return ListTile( + title: Text( + logMessage.message, + style: TextStyle( + color: ctx.colorScheme.onSurface, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Text( + "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}", + style: TextStyle( + color: ctx.colorScheme.onSurfaceSecondary, + fontSize: 12.0, + ), + ), + dense: true, + visualDensity: VisualDensity.compact, + tileColor: Colors.transparent, + minLeadingWidth: 10, + ); + }, + separatorBuilder: (_, index) { + return const Divider(height: 0); + }, + itemCount: logMessages.data?.length ?? 0, + ); + }, + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart new file mode 100644 index 00000000000..5debeff31d8 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -0,0 +1,167 @@ +// ignore_for_file: prefer-single-widget-per-file + +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/domain/models/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +class _Stat { + const _Stat({required this.name, required this.load}); + + final String name; + final Future Function(Drift _) load; +} + +class _Summary extends StatelessWidget { + final String name; + final Future countFuture; + + const _Summary({required this.name, required this.countFuture}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: countFuture, + builder: (ctx, snapshot) { + final Widget subtitle; + + if (snapshot.connectionState == ConnectionState.waiting) { + subtitle = const CircularProgressIndicator(); + } else if (snapshot.hasError) { + subtitle = const Icon(Icons.error_rounded); + } else { + subtitle = Text('${snapshot.data ?? 0}'); + } + return ListTile(title: Text(name), trailing: subtitle); + }, + ); + } +} + +final _localStats = [ + _Stat( + name: 'Local Assets', + load: (db) => db.managers.localAssetEntity.count(), + ), + _Stat( + name: 'Local Albums', + load: (db) => db.managers.localAlbumEntity.count(), + ), +]; + +@RoutePage() +class LocalMediaSummaryPage extends StatelessWidget { + const LocalMediaSummaryPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Local Media Summary')), + body: Consumer( + builder: (ctx, ref, __) { + final db = ref.watch(driftProvider); + final albumsFuture = ref.watch(localAlbumRepository).getAll(); + + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final stat = _localStats[index]; + final countFuture = stat.load(db); + return _Summary(name: stat.name, countFuture: countFuture); + }, + itemCount: _localStats.length, + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 15), + child: Text( + "Album summary", + style: ctx.textTheme.titleMedium, + ), + ), + ], + ), + ), + FutureBuilder( + future: albumsFuture, + initialData: [], + builder: (_, snap) { + final albums = snap.data!; + if (albums.isEmpty) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + albums.sortBy((a) => a.name); + return SliverList.builder( + itemBuilder: (_, index) { + final album = albums[index]; + final countFuture = db.managers.localAlbumAssetEntity + .filter((f) => f.albumId.id.equals(album.id)) + .count(); + return _Summary( + name: album.name, + countFuture: countFuture, + ); + }, + itemCount: albums.length, + ); + }, + ), + ], + ); + }, + ), + ); + } +} + +final _remoteStats = [ + _Stat( + name: 'Remote Assets', + load: (db) => db.managers.remoteAssetEntity.count(), + ), + _Stat( + name: 'Exif Entities', + load: (db) => db.managers.remoteExifEntity.count(), + ), +]; + +@RoutePage() +class RemoteMediaSummaryPage extends StatelessWidget { + const RemoteMediaSummaryPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Remote Media Summary')), + body: Consumer( + builder: (ctx, ref, __) { + final db = ref.watch(driftProvider); + + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final stat = _remoteStats[index]; + final countFuture = stat.load(db); + return _Summary(name: stat.name, countFuture: countFuture); + }, + itemCount: _remoteStats.length, + ), + ], + ); + }, + ), + ); + } +} diff --git a/mobile/lib/providers/backup/backup_album.provider.dart b/mobile/lib/providers/backup/backup_album.provider.dart new file mode 100644 index 00000000000..52ad8857350 --- /dev/null +++ b/mobile/lib/providers/backup/backup_album.provider.dart @@ -0,0 +1,62 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/services/local_album.service.dart'; + +final backupAlbumProvider = + StateNotifierProvider>( + (ref) => BackupAlbumNotifier( + ref.watch(localAlbumsServiceProvider), + ), +); + +class BackupAlbumNotifier extends StateNotifier> { + BackupAlbumNotifier(this._localAlbumService) : super([]) { + getAll(); + } + + final LocalAlbumService _localAlbumService; + + Future getAll() async { + state = await _localAlbumService.getAll(); + } + + Future selectAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.selected); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.selected) + : currentAlbum, + ) + .toList(); + } + + Future deselectAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.none); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.none) + : currentAlbum, + ) + .toList(); + } + + Future excludeAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.excluded); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.excluded) + : currentAlbum, + ) + .toList(); + } +} diff --git a/mobile/lib/providers/backup/upload.provider.dart b/mobile/lib/providers/backup/upload.provider.dart new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart new file mode 100644 index 00000000000..cb4aadb8a75 --- /dev/null +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -0,0 +1,8 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final localAlbumRepository = Provider( + (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart new file mode 100644 index 00000000000..477046d0bf3 --- /dev/null +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -0,0 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; + +final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); diff --git a/mobile/lib/providers/infrastructure/sync_stream.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart similarity index 64% rename from mobile/lib/providers/infrastructure/sync_stream.provider.dart rename to mobile/lib/providers/infrastructure/sync.provider.dart index e313982a301..96e470eba26 100644 --- a/mobile/lib/providers/infrastructure/sync_stream.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -1,10 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/local_sync.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.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/store.provider.dart'; final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( @@ -21,3 +25,11 @@ final syncApiRepositoryProvider = Provider( final syncStreamRepositoryProvider = Provider( (ref) => DriftSyncStreamRepository(ref.watch(driftProvider)), ); + +final localSyncServiceProvider = Provider( + (ref) => LocalSyncService( + localAlbumRepository: ref.watch(localAlbumRepository), + nativeSyncApi: ref.watch(nativeSyncApiProvider), + storeService: ref.watch(storeServiceProvider), + ), +); diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart index 8673ebd2b04..9cba0725805 100644 --- a/mobile/lib/repositories/album_media.repository.dart +++ b/mobile/lib/repositories/album_media.repository.dart @@ -17,6 +17,30 @@ class AlbumMediaRepository implements IAlbumMediaRepository { bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, false); + 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; + @override Future> getAll() async { final filter = useCustomFilter @@ -30,7 +54,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository { @override Future> getAssetIds(String albumId) async { - final album = await AssetPathEntity.fromId(albumId); + 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(); @@ -38,7 +63,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository { @override Future getAssetCount(String albumId) async { - final album = await AssetPathEntity.fromId(albumId); + final album = + await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); return album.assetCountAsync; } @@ -53,17 +79,14 @@ class AlbumMediaRepository implements IAlbumMediaRepository { }) async { final onDevice = await AssetPathEntity.fromId( albumId, - filterOption: FilterOptionGroup( - imageOption: const FilterOption(needTitle: true), - videoOption: const FilterOption(needTitle: true), - containsPathModified: true, + filterOption: _getAlbumFilter( updateTimeCond: modifiedFrom == null && modifiedUntil == null ? null : DateTimeCond( min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760), ), - orders: orderByModificationDate + orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], ), @@ -80,7 +103,10 @@ class AlbumMediaRepository implements IAlbumMediaRepository { DateTime? modifiedFrom, DateTime? modifiedUntil, }) async { - final assetPathEntity = await AssetPathEntity.fromId(id); + final assetPathEntity = await AssetPathEntity.fromId( + id, + filterOption: _getAlbumFilter(containsPathModified: true), + ); return _toAlbum(assetPathEntity); } diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index f9e82e1635a..01d2684faf5 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:drift/drift.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'; @@ -8,17 +9,22 @@ 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/interfaces/auth.interface.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) => + AuthRepository(ref.watch(dbProvider), drift: ref.watch(driftProvider)), ); class AuthRepository extends DatabaseRepository implements IAuthRepository { - AuthRepository(super.db); + final Drift _drift; + + AuthRepository(super.db, {required Drift drift}) : _drift = drift; @override Future clearLocalData() { @@ -29,6 +35,8 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), + _drift.remoteAssetEntity.deleteAll(), + _drift.remoteExifEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart index 039013f7d7f..aa5bdeb4e40 100644 --- a/mobile/lib/repositories/timeline.repository.dart +++ b/mobile/lib/repositories/timeline.repository.dart @@ -98,8 +98,10 @@ class TimelineRepository extends DatabaseRepository } @override - Stream watchAllVideosTimeline() { + Stream watchAllVideosTimeline(String userId) { final query = db.assets + .where() + .ownerIdEqualToAnyChecksum(fastHash(userId)) .filter() .isTrashedEqualTo(false) .visibilityEqualTo(AssetVisibilityEnum.timeline) diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 317ce7cc542..1f14aaa5bfb 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -63,6 +63,8 @@ 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/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/media_stat.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'; @@ -316,5 +318,17 @@ class AppRouter extends RootStackRouter { page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: FeatInDevRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: LocalMediaSummaryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: RemoteMediaSummaryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index da488779e6f..0c57949f04c 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1,3 +1,4 @@ +// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** @@ -13,10 +14,7 @@ part of 'router.dart'; /// [ActivitiesPage] class ActivitiesRoute extends PageRouteInfo { const ActivitiesRoute({List? children}) - : super( - ActivitiesRoute.name, - initialChildren: children, - ); + : super(ActivitiesRoute.name, initialChildren: children); static const String name = 'ActivitiesRoute'; @@ -132,10 +130,7 @@ class AlbumAssetSelectionRouteArgs { /// [AlbumOptionsPage] class AlbumOptionsRoute extends PageRouteInfo { const AlbumOptionsRoute({List? children}) - : super( - AlbumOptionsRoute.name, - initialChildren: children, - ); + : super(AlbumOptionsRoute.name, initialChildren: children); static const String name = 'AlbumOptionsRoute'; @@ -156,10 +151,7 @@ class AlbumPreviewRoute extends PageRouteInfo { List? children, }) : super( AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs( - key: key, - album: album, - ), + args: AlbumPreviewRouteArgs(key: key, album: album), initialChildren: children, ); @@ -169,19 +161,13 @@ class AlbumPreviewRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AlbumPreviewPage( - key: args.key, - album: args.album, - ); + return AlbumPreviewPage(key: args.key, album: args.album); }, ); } class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({ - this.key, - required this.album, - }); + const AlbumPreviewRouteArgs({this.key, required this.album}); final Key? key; @@ -203,10 +189,7 @@ class AlbumSharedUserSelectionRoute List? children, }) : super( AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs( - key: key, - assets: assets, - ), + args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), initialChildren: children, ); @@ -216,19 +199,13 @@ class AlbumSharedUserSelectionRoute name, builder: (data) { final args = data.argsAs(); - return AlbumSharedUserSelectionPage( - key: args.key, - assets: args.assets, - ); + return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); }, ); } class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({ - this.key, - required this.assets, - }); + const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); final Key? key; @@ -249,10 +226,7 @@ class AlbumViewerRoute extends PageRouteInfo { List? children, }) : super( AlbumViewerRoute.name, - args: AlbumViewerRouteArgs( - key: key, - albumId: albumId, - ), + args: AlbumViewerRouteArgs(key: key, albumId: albumId), initialChildren: children, ); @@ -262,19 +236,13 @@ class AlbumViewerRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AlbumViewerPage( - key: args.key, - albumId: args.albumId, - ); + return AlbumViewerPage(key: args.key, albumId: args.albumId); }, ); } class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({ - this.key, - required this.albumId, - }); + const AlbumViewerRouteArgs({this.key, required this.albumId}); final Key? key; @@ -290,10 +258,7 @@ class AlbumViewerRouteArgs { /// [AlbumsPage] class AlbumsRoute extends PageRouteInfo { const AlbumsRoute({List? children}) - : super( - AlbumsRoute.name, - initialChildren: children, - ); + : super(AlbumsRoute.name, initialChildren: children); static const String name = 'AlbumsRoute'; @@ -309,10 +274,7 @@ class AlbumsRoute extends PageRouteInfo { /// [AllMotionPhotosPage] class AllMotionPhotosRoute extends PageRouteInfo { const AllMotionPhotosRoute({List? children}) - : super( - AllMotionPhotosRoute.name, - initialChildren: children, - ); + : super(AllMotionPhotosRoute.name, initialChildren: children); static const String name = 'AllMotionPhotosRoute'; @@ -328,10 +290,7 @@ class AllMotionPhotosRoute extends PageRouteInfo { /// [AllPeoplePage] class AllPeopleRoute extends PageRouteInfo { const AllPeopleRoute({List? children}) - : super( - AllPeopleRoute.name, - initialChildren: children, - ); + : super(AllPeopleRoute.name, initialChildren: children); static const String name = 'AllPeopleRoute'; @@ -347,10 +306,7 @@ class AllPeopleRoute extends PageRouteInfo { /// [AllPlacesPage] class AllPlacesRoute extends PageRouteInfo { const AllPlacesRoute({List? children}) - : super( - AllPlacesRoute.name, - initialChildren: children, - ); + : super(AllPlacesRoute.name, initialChildren: children); static const String name = 'AllPlacesRoute'; @@ -366,10 +322,7 @@ class AllPlacesRoute extends PageRouteInfo { /// [AllVideosPage] class AllVideosRoute extends PageRouteInfo { const AllVideosRoute({List? children}) - : super( - AllVideosRoute.name, - initialChildren: children, - ); + : super(AllVideosRoute.name, initialChildren: children); static const String name = 'AllVideosRoute'; @@ -390,10 +343,7 @@ class AppLogDetailRoute extends PageRouteInfo { List? children, }) : super( AppLogDetailRoute.name, - args: AppLogDetailRouteArgs( - key: key, - logMessage: logMessage, - ), + args: AppLogDetailRouteArgs(key: key, logMessage: logMessage), initialChildren: children, ); @@ -403,19 +353,13 @@ class AppLogDetailRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AppLogDetailPage( - key: args.key, - logMessage: args.logMessage, - ); + return AppLogDetailPage(key: args.key, logMessage: args.logMessage); }, ); } class AppLogDetailRouteArgs { - const AppLogDetailRouteArgs({ - this.key, - required this.logMessage, - }); + const AppLogDetailRouteArgs({this.key, required this.logMessage}); final Key? key; @@ -431,10 +375,7 @@ class AppLogDetailRouteArgs { /// [AppLogPage] class AppLogRoute extends PageRouteInfo { const AppLogRoute({List? children}) - : super( - AppLogRoute.name, - initialChildren: children, - ); + : super(AppLogRoute.name, initialChildren: children); static const String name = 'AppLogRoute'; @@ -450,10 +391,7 @@ class AppLogRoute extends PageRouteInfo { /// [ArchivePage] class ArchiveRoute extends PageRouteInfo { const ArchiveRoute({List? children}) - : super( - ArchiveRoute.name, - initialChildren: children, - ); + : super(ArchiveRoute.name, initialChildren: children); static const String name = 'ArchiveRoute'; @@ -469,10 +407,7 @@ class ArchiveRoute extends PageRouteInfo { /// [BackupAlbumSelectionPage] class BackupAlbumSelectionRoute extends PageRouteInfo { const BackupAlbumSelectionRoute({List? children}) - : super( - BackupAlbumSelectionRoute.name, - initialChildren: children, - ); + : super(BackupAlbumSelectionRoute.name, initialChildren: children); static const String name = 'BackupAlbumSelectionRoute'; @@ -488,10 +423,7 @@ class BackupAlbumSelectionRoute extends PageRouteInfo { /// [BackupControllerPage] class BackupControllerRoute extends PageRouteInfo { const BackupControllerRoute({List? children}) - : super( - BackupControllerRoute.name, - initialChildren: children, - ); + : super(BackupControllerRoute.name, initialChildren: children); static const String name = 'BackupControllerRoute'; @@ -507,10 +439,7 @@ class BackupControllerRoute extends PageRouteInfo { /// [BackupOptionsPage] class BackupOptionsRoute extends PageRouteInfo { const BackupOptionsRoute({List? children}) - : super( - BackupOptionsRoute.name, - initialChildren: children, - ); + : super(BackupOptionsRoute.name, initialChildren: children); static const String name = 'BackupOptionsRoute'; @@ -526,10 +455,7 @@ class BackupOptionsRoute extends PageRouteInfo { /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { const ChangePasswordRoute({List? children}) - : super( - ChangePasswordRoute.name, - initialChildren: children, - ); + : super(ChangePasswordRoute.name, initialChildren: children); static const String name = 'ChangePasswordRoute'; @@ -550,10 +476,7 @@ class CreateAlbumRoute extends PageRouteInfo { List? children, }) : super( CreateAlbumRoute.name, - args: CreateAlbumRouteArgs( - key: key, - assets: assets, - ), + args: CreateAlbumRouteArgs(key: key, assets: assets), initialChildren: children, ); @@ -563,20 +486,15 @@ class CreateAlbumRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs()); - return CreateAlbumPage( - key: args.key, - assets: args.assets, + orElse: () => const CreateAlbumRouteArgs(), ); + return CreateAlbumPage(key: args.key, assets: args.assets); }, ); } class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({ - this.key, - this.assets, - }); + const CreateAlbumRouteArgs({this.key, this.assets}); final Key? key; @@ -598,11 +516,7 @@ class CropImageRoute extends PageRouteInfo { List? children, }) : super( CropImageRoute.name, - args: CropImageRouteArgs( - key: key, - image: image, - asset: asset, - ), + args: CropImageRouteArgs(key: key, image: image, asset: asset), initialChildren: children, ); @@ -612,11 +526,7 @@ class CropImageRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return CropImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); + return CropImagePage(key: args.key, image: args.image, asset: args.asset); }, ); } @@ -702,10 +612,7 @@ class EditImageRouteArgs { /// [FailedBackupStatusPage] class FailedBackupStatusRoute extends PageRouteInfo { const FailedBackupStatusRoute({List? children}) - : super( - FailedBackupStatusRoute.name, - initialChildren: children, - ); + : super(FailedBackupStatusRoute.name, initialChildren: children); static const String name = 'FailedBackupStatusRoute'; @@ -721,10 +628,7 @@ class FailedBackupStatusRoute extends PageRouteInfo { /// [FavoritesPage] class FavoritesRoute extends PageRouteInfo { const FavoritesRoute({List? children}) - : super( - FavoritesRoute.name, - initialChildren: children, - ); + : super(FavoritesRoute.name, initialChildren: children); static const String name = 'FavoritesRoute'; @@ -736,6 +640,22 @@ class FavoritesRoute extends PageRouteInfo { ); } +/// generated route for +/// [FeatInDevPage] +class FeatInDevRoute extends PageRouteInfo { + const FeatInDevRoute({List? children}) + : super(FeatInDevRoute.name, initialChildren: children); + + static const String name = 'FeatInDevRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const FeatInDevPage(); + }, + ); +} + /// generated route for /// [FilterImagePage] class FilterImageRoute extends PageRouteInfo { @@ -746,11 +666,7 @@ class FilterImageRoute extends PageRouteInfo { List? children, }) : super( FilterImageRoute.name, - args: FilterImageRouteArgs( - key: key, - image: image, - asset: asset, - ), + args: FilterImageRouteArgs(key: key, image: image, asset: asset), initialChildren: children, ); @@ -797,10 +713,7 @@ class FolderRoute extends PageRouteInfo { List? children, }) : super( FolderRoute.name, - args: FolderRouteArgs( - key: key, - folder: folder, - ), + args: FolderRouteArgs(key: key, folder: folder), initialChildren: children, ); @@ -809,21 +722,16 @@ class FolderRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const FolderRouteArgs()); - return FolderPage( - key: args.key, - folder: args.folder, + final args = data.argsAs( + orElse: () => const FolderRouteArgs(), ); + return FolderPage(key: args.key, folder: args.folder); }, ); } class FolderRouteArgs { - const FolderRouteArgs({ - this.key, - this.folder, - }); + const FolderRouteArgs({this.key, this.folder}); final Key? key; @@ -903,10 +811,7 @@ class GalleryViewerRouteArgs { /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo { const HeaderSettingsRoute({List? children}) - : super( - HeaderSettingsRoute.name, - initialChildren: children, - ); + : super(HeaderSettingsRoute.name, initialChildren: children); static const String name = 'HeaderSettingsRoute'; @@ -922,10 +827,7 @@ class HeaderSettingsRoute extends PageRouteInfo { /// [LibraryPage] class LibraryRoute extends PageRouteInfo { const LibraryRoute({List? children}) - : super( - LibraryRoute.name, - initialChildren: children, - ); + : super(LibraryRoute.name, initialChildren: children); static const String name = 'LibraryRoute'; @@ -941,10 +843,7 @@ class LibraryRoute extends PageRouteInfo { /// [LocalAlbumsPage] class LocalAlbumsRoute extends PageRouteInfo { const LocalAlbumsRoute({List? children}) - : super( - LocalAlbumsRoute.name, - initialChildren: children, - ); + : super(LocalAlbumsRoute.name, initialChildren: children); static const String name = 'LocalAlbumsRoute'; @@ -956,14 +855,27 @@ class LocalAlbumsRoute extends PageRouteInfo { ); } +/// generated route for +/// [LocalMediaSummaryPage] +class LocalMediaSummaryRoute extends PageRouteInfo { + const LocalMediaSummaryRoute({List? children}) + : super(LocalMediaSummaryRoute.name, initialChildren: children); + + static const String name = 'LocalMediaSummaryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LocalMediaSummaryPage(); + }, + ); +} + /// generated route for /// [LockedPage] class LockedRoute extends PageRouteInfo { const LockedRoute({List? children}) - : super( - LockedRoute.name, - initialChildren: children, - ); + : super(LockedRoute.name, initialChildren: children); static const String name = 'LockedRoute'; @@ -979,10 +891,7 @@ class LockedRoute extends PageRouteInfo { /// [LoginPage] class LoginRoute extends PageRouteInfo { const LoginRoute({List? children}) - : super( - LoginRoute.name, - initialChildren: children, - ); + : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; @@ -1016,7 +925,8 @@ class MapLocationPickerRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const MapLocationPickerRouteArgs()); + orElse: () => const MapLocationPickerRouteArgs(), + ); return MapLocationPickerPage( key: args.key, initialLatLng: args.initialLatLng, @@ -1044,16 +954,10 @@ class MapLocationPickerRouteArgs { /// generated route for /// [MapPage] class MapRoute extends PageRouteInfo { - MapRoute({ - Key? key, - LatLng? initialLocation, - List? children, - }) : super( + MapRoute({Key? key, LatLng? initialLocation, List? children}) + : super( MapRoute.name, - args: MapRouteArgs( - key: key, - initialLocation: initialLocation, - ), + args: MapRouteArgs(key: key, initialLocation: initialLocation), initialChildren: children, ); @@ -1062,21 +966,16 @@ class MapRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const MapRouteArgs()); - return MapPage( - key: args.key, - initialLocation: args.initialLocation, + final args = data.argsAs( + orElse: () => const MapRouteArgs(), ); + return MapPage(key: args.key, initialLocation: args.initialLocation); }, ); } class MapRouteArgs { - const MapRouteArgs({ - this.key, - this.initialLocation, - }); + const MapRouteArgs({this.key, this.initialLocation}); final Key? key; @@ -1213,10 +1112,7 @@ class PartnerDetailRoute extends PageRouteInfo { List? children, }) : super( PartnerDetailRoute.name, - args: PartnerDetailRouteArgs( - key: key, - partner: partner, - ), + args: PartnerDetailRouteArgs(key: key, partner: partner), initialChildren: children, ); @@ -1226,19 +1122,13 @@ class PartnerDetailRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return PartnerDetailPage( - key: args.key, - partner: args.partner, - ); + return PartnerDetailPage(key: args.key, partner: args.partner); }, ); } class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({ - this.key, - required this.partner, - }); + const PartnerDetailRouteArgs({this.key, required this.partner}); final Key? key; @@ -1254,10 +1144,7 @@ class PartnerDetailRouteArgs { /// [PartnerPage] class PartnerRoute extends PageRouteInfo { const PartnerRoute({List? children}) - : super( - PartnerRoute.name, - initialChildren: children, - ); + : super(PartnerRoute.name, initialChildren: children); static const String name = 'PartnerRoute'; @@ -1273,10 +1160,7 @@ class PartnerRoute extends PageRouteInfo { /// [PeopleCollectionPage] class PeopleCollectionRoute extends PageRouteInfo { const PeopleCollectionRoute({List? children}) - : super( - PeopleCollectionRoute.name, - initialChildren: children, - ); + : super(PeopleCollectionRoute.name, initialChildren: children); static const String name = 'PeopleCollectionRoute'; @@ -1292,10 +1176,7 @@ class PeopleCollectionRoute extends PageRouteInfo { /// [PermissionOnboardingPage] class PermissionOnboardingRoute extends PageRouteInfo { const PermissionOnboardingRoute({List? children}) - : super( - PermissionOnboardingRoute.name, - initialChildren: children, - ); + : super(PermissionOnboardingRoute.name, initialChildren: children); static const String name = 'PermissionOnboardingRoute'; @@ -1363,10 +1244,7 @@ class PersonResultRouteArgs { /// [PhotosPage] class PhotosRoute extends PageRouteInfo { const PhotosRoute({List? children}) - : super( - PhotosRoute.name, - initialChildren: children, - ); + : super(PhotosRoute.name, initialChildren: children); static const String name = 'PhotosRoute'; @@ -1387,10 +1265,7 @@ class PinAuthRoute extends PageRouteInfo { List? children, }) : super( PinAuthRoute.name, - args: PinAuthRouteArgs( - key: key, - createPinCode: createPinCode, - ), + args: PinAuthRouteArgs(key: key, createPinCode: createPinCode), initialChildren: children, ); @@ -1399,21 +1274,16 @@ class PinAuthRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const PinAuthRouteArgs()); - return PinAuthPage( - key: args.key, - createPinCode: args.createPinCode, + final args = data.argsAs( + orElse: () => const PinAuthRouteArgs(), ); + return PinAuthPage(key: args.key, createPinCode: args.createPinCode); }, ); } class PinAuthRouteArgs { - const PinAuthRouteArgs({ - this.key, - this.createPinCode = false, - }); + const PinAuthRouteArgs({this.key, this.createPinCode = false}); final Key? key; @@ -1447,7 +1317,8 @@ class PlacesCollectionRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs()); + orElse: () => const PlacesCollectionRouteArgs(), + ); return PlacesCollectionPage( key: args.key, currentLocation: args.currentLocation, @@ -1457,10 +1328,7 @@ class PlacesCollectionRoute extends PageRouteInfo { } class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({ - this.key, - this.currentLocation, - }); + const PlacesCollectionRouteArgs({this.key, this.currentLocation}); final Key? key; @@ -1476,10 +1344,7 @@ class PlacesCollectionRouteArgs { /// [RecentlyTakenPage] class RecentlyTakenRoute extends PageRouteInfo { const RecentlyTakenRoute({List? children}) - : super( - RecentlyTakenRoute.name, - initialChildren: children, - ); + : super(RecentlyTakenRoute.name, initialChildren: children); static const String name = 'RecentlyTakenRoute'; @@ -1491,6 +1356,22 @@ class RecentlyTakenRoute extends PageRouteInfo { ); } +/// generated route for +/// [RemoteMediaSummaryPage] +class RemoteMediaSummaryRoute extends PageRouteInfo { + const RemoteMediaSummaryRoute({List? children}) + : super(RemoteMediaSummaryRoute.name, initialChildren: children); + + static const String name = 'RemoteMediaSummaryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const RemoteMediaSummaryPage(); + }, + ); +} + /// generated route for /// [SearchPage] class SearchRoute extends PageRouteInfo { @@ -1500,10 +1381,7 @@ class SearchRoute extends PageRouteInfo { List? children, }) : super( SearchRoute.name, - args: SearchRouteArgs( - key: key, - prefilter: prefilter, - ), + args: SearchRouteArgs(key: key, prefilter: prefilter), initialChildren: children, ); @@ -1512,21 +1390,16 @@ class SearchRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const SearchRouteArgs()); - return SearchPage( - key: args.key, - prefilter: args.prefilter, + final args = data.argsAs( + orElse: () => const SearchRouteArgs(), ); + return SearchPage(key: args.key, prefilter: args.prefilter); }, ); } class SearchRouteArgs { - const SearchRouteArgs({ - this.key, - this.prefilter, - }); + const SearchRouteArgs({this.key, this.prefilter}); final Key? key; @@ -1542,10 +1415,7 @@ class SearchRouteArgs { /// [SettingsPage] class SettingsRoute extends PageRouteInfo { const SettingsRoute({List? children}) - : super( - SettingsRoute.name, - initialChildren: children, - ); + : super(SettingsRoute.name, initialChildren: children); static const String name = 'SettingsRoute'; @@ -1566,10 +1436,7 @@ class SettingsSubRoute extends PageRouteInfo { List? children, }) : super( SettingsSubRoute.name, - args: SettingsSubRouteArgs( - section: section, - key: key, - ), + args: SettingsSubRouteArgs(section: section, key: key), initialChildren: children, ); @@ -1579,19 +1446,13 @@ class SettingsSubRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return SettingsSubPage( - args.section, - key: args.key, - ); + return SettingsSubPage(args.section, key: args.key); }, ); } class SettingsSubRouteArgs { - const SettingsSubRouteArgs({ - required this.section, - this.key, - }); + const SettingsSubRouteArgs({required this.section, this.key}); final SettingSection section; @@ -1612,10 +1473,7 @@ class ShareIntentRoute extends PageRouteInfo { List? children, }) : super( ShareIntentRoute.name, - args: ShareIntentRouteArgs( - key: key, - attachments: attachments, - ), + args: ShareIntentRouteArgs(key: key, attachments: attachments), initialChildren: children, ); @@ -1625,19 +1483,13 @@ class ShareIntentRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return ShareIntentPage( - key: args.key, - attachments: args.attachments, - ); + return ShareIntentPage(key: args.key, attachments: args.attachments); }, ); } class ShareIntentRouteArgs { - const ShareIntentRouteArgs({ - this.key, - required this.attachments, - }); + const ShareIntentRouteArgs({this.key, required this.attachments}); final Key? key; @@ -1675,7 +1527,8 @@ class SharedLinkEditRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const SharedLinkEditRouteArgs()); + orElse: () => const SharedLinkEditRouteArgs(), + ); return SharedLinkEditPage( key: args.key, existingLink: args.existingLink, @@ -1712,10 +1565,7 @@ class SharedLinkEditRouteArgs { /// [SharedLinkPage] class SharedLinkRoute extends PageRouteInfo { const SharedLinkRoute({List? children}) - : super( - SharedLinkRoute.name, - initialChildren: children, - ); + : super(SharedLinkRoute.name, initialChildren: children); static const String name = 'SharedLinkRoute'; @@ -1731,10 +1581,7 @@ class SharedLinkRoute extends PageRouteInfo { /// [SplashScreenPage] class SplashScreenRoute extends PageRouteInfo { const SplashScreenRoute({List? children}) - : super( - SplashScreenRoute.name, - initialChildren: children, - ); + : super(SplashScreenRoute.name, initialChildren: children); static const String name = 'SplashScreenRoute'; @@ -1750,10 +1597,7 @@ class SplashScreenRoute extends PageRouteInfo { /// [TabControllerPage] class TabControllerRoute extends PageRouteInfo { const TabControllerRoute({List? children}) - : super( - TabControllerRoute.name, - initialChildren: children, - ); + : super(TabControllerRoute.name, initialChildren: children); static const String name = 'TabControllerRoute'; @@ -1769,10 +1613,7 @@ class TabControllerRoute extends PageRouteInfo { /// [TrashPage] class TrashRoute extends PageRouteInfo { const TrashRoute({List? children}) - : super( - TrashRoute.name, - initialChildren: children, - ); + : super(TrashRoute.name, initialChildren: children); static const String name = 'TrashRoute'; diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 6413b69fce4..b6c675b6362 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -104,7 +104,7 @@ class AppSettingsService { return Store.get(setting.storeKey, setting.defaultValue); } - void setSetting(AppSettingsEnum setting, T value) { - Store.put(setting.storeKey, value); + Future setSetting(AppSettingsEnum setting, T value) { + return Store.put(setting.storeKey, value); } } diff --git a/mobile/lib/services/local_album.service.dart b/mobile/lib/services/local_album.service.dart new file mode 100644 index 00000000000..44a74d1652b --- /dev/null +++ b/mobile/lib/services/local_album.service.dart @@ -0,0 +1,24 @@ +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +final localAlbumsServiceProvider = Provider( + (ref) => LocalAlbumService( + ref.watch(localAlbumRepository), + ), +); + +class LocalAlbumService { + LocalAlbumService(this._localAlbumRepository); + + final ILocalAlbumRepository _localAlbumRepository; + + Future> getAll() { + return _localAlbumRepository.getAll(); + } + + Future update(LocalAlbum album) { + return _localAlbumRepository.update(album); + } +} diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart index 7ecad43ca70..d7d1e7cf252 100644 --- a/mobile/lib/services/timeline.service.dart +++ b/mobile/lib/services/timeline.service.dart @@ -75,7 +75,9 @@ class TimelineService { } Stream watchAllVideosTimeline() { - return _timelineRepository.watchAllVideosTimeline(); + final user = _userService.getMyUser(); + + return _timelineRepository.watchAllVideosTimeline(user.id); } Future getTimelineFromAssets( diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 7c7d9bab882..58c3ef83947 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -11,6 +11,7 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'people', PeopleResponse().toJson()); addDefault(value, 'tags', TagsResponse().toJson()); addDefault(value, 'sharedLinks', SharedLinksResponse().toJson()); + addDefault(value, 'cast', CastResponse().toJson()); } break; case 'ServerConfigDto': diff --git a/mobile/lib/utils/translation.dart b/mobile/lib/utils/translation.dart index 461e88ead70..1a33161dbcd 100644 --- a/mobile/lib/utils/translation.dart +++ b/mobile/lib/utils/translation.dart @@ -5,7 +5,8 @@ String t(String key, [Map? args]) { try { String message = key.tr(); if (args != null) { - return MessageFormat(message).format(args); + return MessageFormat(message, locale: Intl.defaultLocale ?? 'en') + .format(args); } return message; } catch (e) { diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 79944ef15fd..9f78b6066dc 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -5,6 +5,7 @@ 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/providers/user.provider.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class AlbumThumbnailCard extends ConsumerWidget { @@ -61,28 +62,24 @@ class AlbumThumbnailCard extends ConsumerWidget { if (album.ownerId == ref.read(currentUserProvider)?.id) { owner = 'owned'.tr(); } else if (album.ownerName != null) { - owner = 'album_thumbnail_shared_by' - .tr(namedArgs: {'user': album.ownerName!}); + owner = t('shared_by_user', {'user': album.ownerName!}); } } - return RichText( - overflow: TextOverflow.fade, - text: TextSpan( - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), + return Text.rich( + TextSpan( children: [ TextSpan( - text: album.assetCount == 1 - ? 'album_thumbnail_card_item'.tr() - : 'album_thumbnail_card_items' - .tr(namedArgs: {'count': '${album.assetCount}'}), + text: t('items_count', {'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, ); } diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart index 17c2a6bd12e..11ef5d329b3 100644 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ b/mobile/lib/widgets/album/album_thumbnail_listtile.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:openapi/api.dart'; class AlbumThumbnailListTile extends StatelessWidget { @@ -90,20 +91,25 @@ class AlbumThumbnailListTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - album.assetCount == 1 - ? 'album_thumbnail_card_item' - : 'album_thumbnail_card_items', + t('items_count', {'count': album.assetCount}), style: const TextStyle( fontSize: 12, ), - ).tr(namedArgs: {'count': '${album.assetCount}'}), - if (album.shared) + ), + if (album.shared) ...[ const Text( - 'album_thumbnail_card_shared', + ' • ', style: TextStyle( fontSize: 12, ), - ).tr(), + ), + Text( + 'shared'.tr(), + style: const TextStyle( + fontSize: 12, + ), + ), + ], ], ), ], diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart index 526d2199be7..f626048a395 100644 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ b/mobile/lib/widgets/backup/album_info_card.dart @@ -3,18 +3,19 @@ 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/domain/models/local_album.model.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/backup/backup_album.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 AlbumInfoCard extends HookConsumerWidget { - final AvailableAlbum album; + final LocalAlbum album; const AlbumInfoCard({ super.key, @@ -23,10 +24,8 @@ class AlbumInfoCard extends HookConsumerWidget { @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 bool isSelected = album.backupSelection == BackupSelection.selected; + final bool isExcluded = album.backupSelection == BackupSelection.excluded; final syncAlbum = ref .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.syncAlbums); @@ -91,9 +90,10 @@ class AlbumInfoCard extends HookConsumerWidget { ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).selectAlbum(album); + if (syncAlbum) { ref.read(albumProvider.notifier).createSyncAlbum(album.name); } @@ -103,11 +103,8 @@ class AlbumInfoCard extends HookConsumerWidget { ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } else { - // Add to exclude album list - if (album.id == 'isAll' || album.name == 'Recents') { ImmichToast.show( context: context, @@ -117,8 +114,7 @@ class AlbumInfoCard extends HookConsumerWidget { ); return; } - - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).excludeAlbum(album); } }, child: Card( @@ -181,25 +177,16 @@ class AlbumInfoCard extends HookConsumerWidget { 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), - ); + // TODO: refactor below + + // context.pushRoute( + // AlbumPreviewRoute(album: album.album), + // ); }, icon: Icon( Icons.image_outlined, diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index a263c004bdf..e10cf5d49b6 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -1,28 +1,25 @@ -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/domain/models/local_album.model.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/backup/backup_album.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/common/immich_toast.dart'; class AlbumInfoListTile extends HookConsumerWidget { - final AvailableAlbum album; + final LocalAlbum 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 bool isSelected = album.backupSelection == BackupSelection.selected; + final bool isExcluded = album.backupSelection == BackupSelection.excluded; + final syncAlbum = ref .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.syncAlbums); @@ -67,11 +64,8 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.watch(hapticFeedbackProvider.notifier).selectionClick(); if (isExcluded) { - // Remove from exclude album list - ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } else { - // Add to exclude album list - if (album.id == 'isAll' || album.name == 'Recents') { ImmichToast.show( context: context, @@ -82,7 +76,7 @@ class AlbumInfoListTile extends HookConsumerWidget { return; } - ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).excludeAlbum(album); } }, child: ListTile( @@ -91,9 +85,9 @@ class AlbumInfoListTile extends HookConsumerWidget { onTap: () { ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isSelected) { - ref.read(backupProvider.notifier).removeAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } else { - ref.read(backupProvider.notifier).addAlbumForBackup(album); + ref.read(backupAlbumProvider.notifier).selectAlbum(album); if (syncAlbum) { ref.read(albumProvider.notifier).createSyncAlbum(album.name); } @@ -110,9 +104,10 @@ class AlbumInfoListTile extends HookConsumerWidget { subtitle: Text(album.assetCount.toString()), trailing: IconButton( onPressed: () { - context.pushRoute( - AlbumPreviewRoute(album: album.album), - ); + // TODO: refactor below + // context.pushRoute( + // AlbumPreviewRoute(album: album.album), + // ); }, icon: Icon( Icons.image_outlined, diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart index 7919e72ea69..04be0c00dcd 100644 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ b/mobile/lib/widgets/backup/ios_debug_info_tile.dart @@ -1,8 +1,9 @@ +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/ios_background_settings.provider.dart'; -import 'package:intl/intl.dart'; +import 'package:immich_mobile/utils/translation.dart'; /// This is a simple debug widget which should be removed later on when we are /// more confident about background sync @@ -19,26 +20,35 @@ class IosDebugInfoTile extends HookConsumerWidget { final processing = settings.timeOfLastProcessing; final processes = settings.numberOfBackgroundTasksQueued; - final processOrProcesses = processes == 1 ? 'process' : 'processes'; - final numberOrZero = processes == 0 ? 'No' : processes.toString(); - final title = '$numberOrZero background $processOrProcesses queued'; + final String title; + if (processes == 0) { + title = 'ios_debug_info_no_processes_queued'.tr(); + } else { + title = t('ios_debug_info_processes_queued', {'count': processes}); + } final df = DateFormat.yMd().add_jm(); final String subtitle; if (fetch == null && processing == null) { - subtitle = 'No background sync job has run yet'; + subtitle = 'ios_debug_info_no_sync_yet'.tr(); } else if (fetch != null && processing == null) { - subtitle = 'Fetch ran ${df.format(fetch)}'; + subtitle = + t('ios_debug_info_fetch_ran_at', {'dateTime': df.format(fetch)}); } else if (processing != null && fetch == null) { - subtitle = 'Processing ran ${df.format(processing)}'; + subtitle = t( + 'ios_debug_info_processing_ran_at', + {'dateTime': df.format(processing)}, + ); } else { final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'Last sync ${df.format(fetchOrProcessing)}'; + subtitle = t( + 'ios_debug_info_last_sync_at', + {'dateTime': df.format(fetchOrProcessing)}, + ); } return ListTile( - key: ValueKey(title), title: Text( title, style: TextStyle( 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 b2b24bd01c6..cc14ffa5fea 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 @@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/asset.provider.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/locale_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'; @@ -23,6 +24,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.watch(localeProvider); BackUpState backupState = ref.watch(backupProvider); final theme = context.themeData; bool isHorizontal = !context.isMobile; diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 848b7879fa0..0f8ae8b8e15 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -16,6 +17,7 @@ class AppBarServerInfo extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.watch(localeProvider); ServerInfo serverInfoState = ref.watch(serverInfoProvider); final appInfo = useState({}); diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 4f95e657d95..09f81b9e1a1 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -7,7 +7,6 @@ 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/models/server_info/server_info.model.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -180,10 +179,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { child: action, ), ), - if (kDebugMode) + if (kDebugMode || kProfileMode) IconButton( - onPressed: () => ref.read(backgroundSyncProvider).sync(), - icon: const Icon(Icons.sync), + icon: const Icon(Icons.science_rounded), + onPressed: () => context.pushRoute(const FeatInDevRoute()), ), if (showUploadButton) Padding( diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart index 80d7427d826..bb892737f6f 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart @@ -120,7 +120,6 @@ class PhotoViewCoreState extends State TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector { - Offset? _normalizedPosition; double? _scaleBefore; double? _rotationBefore; @@ -153,23 +152,29 @@ class PhotoViewCoreState extends State void onScaleStart(ScaleStartDetails details) { _rotationBefore = controller.rotation; _scaleBefore = scale; - _normalizedPosition = details.focalPoint - controller.position; _scaleAnimationController.stop(); _positionAnimationController.stop(); _rotationAnimationController.stop(); } void onScaleUpdate(ScaleUpdateDetails details) { + final centeredFocalPoint = Offset( + details.focalPoint.dx - scaleBoundaries.outerSize.width / 2, + details.focalPoint.dy - scaleBoundaries.outerSize.height / 2, + ); final double newScale = _scaleBefore! * details.scale; - final Offset delta = details.focalPoint - _normalizedPosition!; + final double scaleDelta = newScale / scale; + final Offset newPosition = + (controller.position + details.focalPointDelta) * scaleDelta - + centeredFocalPoint * (scaleDelta - 1); updateScaleStateFromNewScale(newScale); updateMultiple( scale: newScale, position: widget.enablePanAlways - ? delta - : clampPosition(position: delta, scale: details.scale), + ? newPosition + : clampPosition(position: newPosition), rotation: widget.enableRotation ? _rotationBefore! + details.rotation : null, rotationFocusPoint: widget.enableRotation ? details.focalPoint : null, diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart index 54a1029f298..b12b9a76349 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart @@ -1,5 +1,4 @@ import 'package:flutter/widgets.dart'; - import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller_delegate.dart' show PhotoViewControllerDelegate; @@ -7,7 +6,7 @@ mixin HitCornersDetector on PhotoViewControllerDelegate { HitCorners _hitCornersX() { final double childWidth = scaleBoundaries.childSize.width * scale; final double screenWidth = scaleBoundaries.outerSize.width; - if (screenWidth >= childWidth) { + if (screenWidth - childWidth > -0.001) { return const HitCorners(true, true); } final x = -position.dx; @@ -18,7 +17,7 @@ mixin HitCornersDetector on PhotoViewControllerDelegate { HitCorners _hitCornersY() { final double childHeight = scaleBoundaries.childSize.height * scale; final double screenHeight = scaleBoundaries.outerSize.height; - if (screenHeight >= childHeight) { + if (screenHeight - childHeight > -0.001) { return const HitCorners(true, true); } final y = -position.dy; 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 8f4455ed884..df974fff30b 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 @@ -1,12 +1,14 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/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/settings_radio_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; class GroupSettings extends HookConsumerWidget { const GroupSettings({ @@ -18,14 +20,18 @@ class GroupSettings extends HookConsumerWidget { final groupByIndex = useAppSettingsState(AppSettingsEnum.groupAssetsBy); final groupBy = GroupAssetsBy.values[groupByIndex.value]; + Future updateAppSettings(GroupAssetsBy groupBy) async { + await ref.watch(appSettingsServiceProvider).setSetting( + AppSettingsEnum.groupAssetsBy, + groupBy.index, + ); + ref.invalidate(appSettingsServiceProvider); + } + void changeGroupValue(GroupAssetsBy? value) { if (value != null) { groupByIndex.value = value.index; - ref.watch(appSettingsServiceProvider).setSetting( - AppSettingsEnum.groupAssetsBy, - value.index, - ); - ref.invalidate(appSettingsServiceProvider); + unawaited(updateAppSettings(groupBy)); } } diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 990dcfdfe8a..7dc7f89ea11 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -1,79 +1,324 @@ -import 'package:easy_localization/easy_localization.dart'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/localization.service.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; class LanguageSettings extends HookConsumerWidget { const LanguageSettings({super.key}); + Future _applyLanguageChange( + BuildContext context, + ValueNotifier selectedLocale, + ValueNotifier isLoading, + ) async { + isLoading.value = true; + await Future.delayed(const Duration(milliseconds: 500)); + try { + await context.setLocale(selectedLocale.value); + await loadTranslations(); + } finally { + isLoading.value = false; + } + } + @override Widget build(BuildContext context, WidgetRef ref) { + final localeEntries = useMemoized(() => locales.entries.toList(), const []); final currentLocale = context.locale; - final textController = useTextEditingController( - text: locales.keys.firstWhere( - (countryName) => locales[countryName] == currentLocale, - ), - ); - + final filteredLocaleEntries = + useState>>(localeEntries); final selectedLocale = useState(currentLocale); - return ListView( - padding: const EdgeInsets.all(16), - children: [ - LayoutBuilder( - builder: (context, constraints) { - return DropdownMenu( - width: constraints.maxWidth, - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - ), - contentPadding: const EdgeInsets.only(left: 16), - ), - menuStyle: MenuStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), + final isLoading = useState(false); + final isButtonDisabled = + selectedLocale.value == currentLocale || isLoading.value; + + final searchController = useTextEditingController(); + final searchFocusNode = useFocusNode(); + final debounceTimer = useRef(null); + + void onSearch(String searchTerm) { + debounceTimer.value?.cancel(); + debounceTimer.value = Timer(const Duration(milliseconds: 500), () { + if (searchTerm.isEmpty) { + filteredLocaleEntries.value = localeEntries; + } else { + filteredLocaleEntries.value = localeEntries + .where( + (entry) => + entry.key.toLowerCase().contains(searchTerm.toLowerCase()), + ) + .toList(); + } + }); + } + + void clearSearch() { + searchController.clear(); + onSearch(''); + } + + useEffect( + () { + void searchListener() => onSearch(searchController.text); + searchController.addListener(searchListener); + return () { + searchController.removeListener(searchListener); + debounceTimer.value?.cancel(); + }; + }, + [searchController], + ); + + return SafeArea( + child: Column( + children: [ + _LanguageSearchBar( + controller: searchController, + focusNode: searchFocusNode, + onClear: clearSearch, + onChanged: (_) => onSearch(searchController.text), + ), + Expanded( + child: filteredLocaleEntries.value.isEmpty + ? const _LanguageNotFound() + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: filteredLocaleEntries.value.length, + itemExtent: 64.0, + itemBuilder: (context, index) { + final countryName = + filteredLocaleEntries.value[index].key; + final localeValue = + filteredLocaleEntries.value[index].value; + final bool isSelected = + selectedLocale.value == localeValue; + + return _LanguageItem( + countryName: countryName, + localeValue: localeValue, + isSelected: isSelected, + onTap: () { + selectedLocale.value = localeValue; + }, + ); + }, ), - ), - backgroundColor: WidgetStatePropertyAll( - context.colorScheme.surfaceContainer, - ), + ), + if (filteredLocaleEntries.value.isNotEmpty) + _LanguageApplyButton( + isDisabled: isButtonDisabled, + isLoading: isLoading.value, + onPressed: () => _applyLanguageChange( + context, + selectedLocale, + isLoading, ), - menuHeight: context.height * 0.5, - hintText: "Languages", - label: const Text('Languages'), - dropdownMenuEntries: locales.keys - .map( - (countryName) => DropdownMenuEntry( - value: locales[countryName], - label: countryName, - ), - ) - .toList(), - controller: textController, - onSelected: (value) { - if (value != null) { - selectedLocale.value = value; - } - }, - ); - }, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: selectedLocale.value == currentLocale - ? null - : () { - context.setLocale(selectedLocale.value); - loadTranslations(); - }, - child: const Text('setting_languages_apply').tr(), - ), - ], + ), + ], + ), + ); + } +} + +class _LanguageSearchBar extends StatelessWidget { + const _LanguageSearchBar({ + required this.controller, + required this.focusNode, + required this.onClear, + required this.onChanged, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final VoidCallback onClear; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 16, bottom: 8, left: 50, right: 50), + decoration: BoxDecoration( + color: context.colorScheme.surface, + ), + child: DecoratedBox( + decoration: BoxDecoration( + 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( + autofocus: false, + contentPadding: const EdgeInsets.all(12), + hintText: 'language_search_hint'.tr(), + prefixIcon: const Icon(Icons.search_rounded), + suffixIcon: controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear_rounded), + onPressed: onClear, + ) + : null, + controller: controller, + onChanged: onChanged, + focusNode: focusNode, + onTapOutside: (_) => focusNode.unfocus(), + ), + ), + ); + } +} + +class _LanguageNotFound extends StatelessWidget { + const _LanguageNotFound(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off_rounded, + size: 64, + color: context.colorScheme.onSurface.withValues(alpha: 0.4), + ), + const SizedBox(height: 8), + Text( + 'language_no_results_title'.tr(), + style: context.textTheme.titleMedium?.copyWith( + color: context.colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + 'language_no_results_subtitle'.tr(), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + ], + ), + ); + } +} + +class _LanguageApplyButton extends StatelessWidget { + const _LanguageApplyButton({ + required this.isDisabled, + required this.isLoading, + required this.onPressed, + }); + + final bool isDisabled; + final bool isLoading; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: context.colorScheme.surface, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: isDisabled ? null : onPressed, + child: isLoading + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Text( + 'setting_languages_apply'.tr(), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + ), + ), + ), + ), + ), + ); + } +} + +class _LanguageItem extends StatelessWidget { + const _LanguageItem({ + required this.countryName, + required this.localeValue, + required this.isSelected, + required this.onTap, + }); + + final String countryName; + final Locale localeValue; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: + context.colorScheme.surfaceContainerLowest.withValues(alpha: .6), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + border: Border.all( + color: context.colorScheme.outlineVariant.withValues(alpha: .4), + width: 1.0, + ), + ), + child: ListTile( + title: Text( + countryName, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? context.colorScheme.primary + : context.colorScheme.onSurfaceVariant, + ), + ), + trailing: isSelected + ? Icon( + Icons.check, + color: context.colorScheme.primary, + size: 20, + ) + : null, + onTap: onTap, + selected: isSelected, + selectedTileColor: context.colorScheme.primary.withValues(alpha: .15), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + ), + ), ); } } diff --git a/mobile/makefile b/mobile/makefile index b0083b1495b..ec0d08f0878 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -1,7 +1,13 @@ -.PHONY: build watch create_app_icon create_splash build_release_android +.PHONY: build watch create_app_icon create_splash build_release_android pigeon build: dart run build_runner build --delete-conflicting-outputs +# Remove once auto_route updated to 10.1.0 + dart format lib/routing/router.gr.dart + +pigeon: + dart run pigeon --input pigeon/native_sync_api.dart + dart format lib/platform/native_sync_api.g.dart watch: dart run build_runner watch --delete-conflicting-outputs @@ -15,8 +21,9 @@ create_splash: build_release_android: flutter build appbundle -migrations: +migration: dart run drift_dev make-migrations translation: - dart run easy_localization:generate -S ../i18n \ No newline at end of file + dart run easy_localization:generate -S ../i18n + dart format lib/generated/codegen_loader.g.dart \ No newline at end of file diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 24faee28ade..4ff55e5db8d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -184,6 +184,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | +*ServerApi* | [**getApkLinks**](doc//ServerApi.md#getapklinks) | **GET** /server/apk-links | *ServerApi* | [**getServerConfig**](doc//ServerApi.md#getserverconfig) | **GET** /server/config | *ServerApi* | [**getServerFeatures**](doc//ServerApi.md#getserverfeatures) | **GET** /server/features | *ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license | @@ -246,13 +247,16 @@ Class | Method | HTTP request | Description *UsersApi* | [**createProfileImage**](doc//UsersApi.md#createprofileimage) | **POST** /users/profile-image | *UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /users/profile-image | *UsersApi* | [**deleteUserLicense**](doc//UsersApi.md#deleteuserlicense) | **DELETE** /users/me/license | +*UsersApi* | [**deleteUserOnboarding**](doc//UsersApi.md#deleteuseronboarding) | **DELETE** /users/me/onboarding | *UsersApi* | [**getMyPreferences**](doc//UsersApi.md#getmypreferences) | **GET** /users/me/preferences | *UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me | *UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image | *UsersApi* | [**getUser**](doc//UsersApi.md#getuser) | **GET** /users/{id} | *UsersApi* | [**getUserLicense**](doc//UsersApi.md#getuserlicense) | **GET** /users/me/license | +*UsersApi* | [**getUserOnboarding**](doc//UsersApi.md#getuseronboarding) | **GET** /users/me/onboarding | *UsersApi* | [**searchUsers**](doc//UsersApi.md#searchusers) | **GET** /users | *UsersApi* | [**setUserLicense**](doc//UsersApi.md#setuserlicense) | **PUT** /users/me/license | +*UsersApi* | [**setUserOnboarding**](doc//UsersApi.md#setuseronboarding) | **PUT** /users/me/onboarding | *UsersApi* | [**updateMyPreferences**](doc//UsersApi.md#updatemypreferences) | **PUT** /users/me/preferences | *UsersApi* | [**updateMyUser**](doc//UsersApi.md#updatemyuser) | **PUT** /users/me | *UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users | @@ -321,6 +325,8 @@ Class | Method | HTTP request | Description - [BulkIdsDto](doc//BulkIdsDto.md) - [CLIPConfig](doc//CLIPConfig.md) - [CQMode](doc//CQMode.md) + - [CastResponse](doc//CastResponse.md) + - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) @@ -382,6 +388,8 @@ Class | Method | HTTP request | Description - [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthTokenEndpointAuthMethod](doc//OAuthTokenEndpointAuthMethod.md) - [OnThisDayDto](doc//OnThisDayDto.md) + - [OnboardingDto](doc//OnboardingDto.md) + - [OnboardingResponseDto](doc//OnboardingResponseDto.md) - [PartnerDirection](doc//PartnerDirection.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PeopleResponse](doc//PeopleResponse.md) @@ -417,6 +425,7 @@ Class | Method | HTTP request | Description - [SearchResponseDto](doc//SearchResponseDto.md) - [SearchSuggestionType](doc//SearchSuggestionType.md) - [ServerAboutResponseDto](doc//ServerAboutResponseDto.md) + - [ServerApkLinksDto](doc//ServerApkLinksDto.md) - [ServerConfigDto](doc//ServerConfigDto.md) - [ServerFeaturesDto](doc//ServerFeaturesDto.md) - [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d3a342db6c1..87d14248ebd 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -114,6 +114,8 @@ part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; part 'model/clip_config.dart'; 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'; @@ -175,6 +177,8 @@ part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; part 'model/o_auth_token_endpoint_auth_method.dart'; part 'model/on_this_day_dto.dart'; +part 'model/onboarding_dto.dart'; +part 'model/onboarding_response_dto.dart'; part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; part 'model/people_response.dart'; @@ -210,6 +214,7 @@ part 'model/search_facet_response_dto.dart'; part 'model/search_response_dto.dart'; part 'model/search_suggestion_type.dart'; part 'model/server_about_response_dto.dart'; +part 'model/server_apk_links_dto.dart'; part 'model/server_config_dto.dart'; part 'model/server_features_dto.dart'; part 'model/server_media_types_response_dto.dart'; diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index a0fd54f3d2b..7abdabcd3e1 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -90,6 +90,47 @@ class ServerApi { return null; } + /// Performs an HTTP 'GET /server/apk-links' operation and returns the [Response]. + Future getApkLinksWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/server/apk-links'; + + // 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, + ); + } + + Future getApkLinks() async { + final response = await getApkLinksWithHttpInfo(); + 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), 'ServerApkLinksDto',) as ServerApkLinksDto; + + } + return null; + } + /// Performs an HTTP 'GET /server/config' operation and returns the [Response]. Future getServerConfigWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index a48ec54cfe5..cd31617e74c 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -139,6 +139,39 @@ class UsersApi { } } + /// Performs an HTTP 'DELETE /users/me/onboarding' operation and returns the [Response]. + Future deleteUserOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future deleteUserOnboarding() async { + final response = await deleteUserOnboardingWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /users/me/preferences' operation and returns the [Response]. Future getMyPreferencesWithHttpInfo() async { // ignore: prefer_const_declarations @@ -358,6 +391,47 @@ class UsersApi { return null; } + /// Performs an HTTP 'GET /users/me/onboarding' operation and returns the [Response]. + Future getUserOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // 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, + ); + } + + Future getUserOnboarding() async { + final response = await getUserOnboardingWithHttpInfo(); + 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), 'OnboardingResponseDto',) as OnboardingResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /users' operation and returns the [Response]. Future searchUsersWithHttpInfo() async { // ignore: prefer_const_declarations @@ -449,6 +523,53 @@ class UsersApi { return null; } + /// Performs an HTTP 'PUT /users/me/onboarding' operation and returns the [Response]. + /// Parameters: + /// + /// * [OnboardingDto] onboardingDto (required): + Future setUserOnboardingWithHttpInfo(OnboardingDto onboardingDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody = onboardingDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [OnboardingDto] onboardingDto (required): + Future setUserOnboarding(OnboardingDto onboardingDto,) async { + final response = await setUserOnboardingWithHttpInfo(onboardingDto,); + 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), 'OnboardingResponseDto',) as OnboardingResponseDto; + + } + return null; + } + /// Performs an HTTP 'PUT /users/me/preferences' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index cc01fd2c064..46936fa88b4 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -284,6 +284,10 @@ class ApiClient { return CLIPConfig.fromJson(value); case 'CQMode': return CQModeTypeTransformer().decode(value); + case 'CastResponse': + return CastResponse.fromJson(value); + case 'CastUpdate': + return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); case 'CheckExistingAssetsDto': @@ -406,6 +410,10 @@ class ApiClient { return OAuthTokenEndpointAuthMethodTypeTransformer().decode(value); case 'OnThisDayDto': return OnThisDayDto.fromJson(value); + case 'OnboardingDto': + return OnboardingDto.fromJson(value); + case 'OnboardingResponseDto': + return OnboardingResponseDto.fromJson(value); case 'PartnerDirection': return PartnerDirectionTypeTransformer().decode(value); case 'PartnerResponseDto': @@ -476,6 +484,8 @@ class ApiClient { return SearchSuggestionTypeTypeTransformer().decode(value); case 'ServerAboutResponseDto': return ServerAboutResponseDto.fromJson(value); + case 'ServerApkLinksDto': + return ServerApkLinksDto.fromJson(value); case 'ServerConfigDto': return ServerConfigDto.fromJson(value); case 'ServerFeaturesDto': diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index 60ac168fdbd..7f32c951187 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -13,11 +13,17 @@ part of openapi.api; class APIKeyUpdateDto { /// Returns a new [APIKeyUpdateDto] instance. APIKeyUpdateDto({ - required this.name, + this.name, this.permissions = const [], }); - String name; + /// + /// 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? name; List permissions; @@ -29,7 +35,7 @@ class APIKeyUpdateDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (name.hashCode) + + (name == null ? 0 : name!.hashCode) + (permissions.hashCode); @override @@ -37,7 +43,11 @@ class APIKeyUpdateDto { Map toJson() { final json = {}; + if (this.name != null) { json[r'name'] = this.name; + } else { + // json[r'name'] = null; + } json[r'permissions'] = this.permissions; return json; } @@ -51,7 +61,7 @@ class APIKeyUpdateDto { final json = value.cast(); return APIKeyUpdateDto( - name: mapValueOfType(json, r'name')!, + name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); } @@ -100,8 +110,6 @@ class APIKeyUpdateDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'name', - 'permissions', }; } diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart new file mode 100644 index 00000000000..d49f1ad3d76 --- /dev/null +++ b/mobile/openapi/lib/model/cast_response.dart @@ -0,0 +1,99 @@ +// +// 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 CastResponse { + /// Returns a new [CastResponse] instance. + CastResponse({ + this.gCastEnabled = false, + }); + + bool gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastResponse && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled.hashCode); + + @override + String toString() => 'CastResponse[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + json[r'gCastEnabled'] = this.gCastEnabled; + return json; + } + + /// Returns a new [CastResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastResponse? fromJson(dynamic value) { + upgradeDto(value, "CastResponse"); + if (value is Map) { + final json = value.cast(); + + return CastResponse( + gCastEnabled: mapValueOfType(json, r'gCastEnabled')!, + ); + } + 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 = CastResponse.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 = CastResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastResponse-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] = CastResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'gCastEnabled', + }; +} + diff --git a/mobile/openapi/lib/model/cast_update.dart b/mobile/openapi/lib/model/cast_update.dart new file mode 100644 index 00000000000..87076391325 --- /dev/null +++ b/mobile/openapi/lib/model/cast_update.dart @@ -0,0 +1,108 @@ +// +// 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 CastUpdate { + /// Returns a new [CastUpdate] instance. + CastUpdate({ + this.gCastEnabled, + }); + + /// + /// 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? gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastUpdate && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled == null ? 0 : gCastEnabled!.hashCode); + + @override + String toString() => 'CastUpdate[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + if (this.gCastEnabled != null) { + json[r'gCastEnabled'] = this.gCastEnabled; + } else { + // json[r'gCastEnabled'] = null; + } + return json; + } + + /// Returns a new [CastUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastUpdate? fromJson(dynamic value) { + upgradeDto(value, "CastUpdate"); + if (value is Map) { + final json = value.cast(); + + return CastUpdate( + gCastEnabled: mapValueOfType(json, r'gCastEnabled'), + ); + } + 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 = CastUpdate.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 = CastUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastUpdate-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] = CastUpdate.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/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index dbc82d07ba1..82a4f9b3ed7 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -15,6 +15,7 @@ class LoginResponseDto { LoginResponseDto({ required this.accessToken, required this.isAdmin, + required this.isOnboarded, required this.name, required this.profileImagePath, required this.shouldChangePassword, @@ -26,6 +27,8 @@ class LoginResponseDto { bool isAdmin; + bool isOnboarded; + String name; String profileImagePath; @@ -40,6 +43,7 @@ class LoginResponseDto { bool operator ==(Object other) => identical(this, other) || other is LoginResponseDto && other.accessToken == accessToken && other.isAdmin == isAdmin && + other.isOnboarded == isOnboarded && other.name == name && other.profileImagePath == profileImagePath && other.shouldChangePassword == shouldChangePassword && @@ -51,6 +55,7 @@ class LoginResponseDto { // ignore: unnecessary_parenthesis (accessToken.hashCode) + (isAdmin.hashCode) + + (isOnboarded.hashCode) + (name.hashCode) + (profileImagePath.hashCode) + (shouldChangePassword.hashCode) + @@ -58,12 +63,13 @@ class LoginResponseDto { (userId.hashCode); @override - String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; + String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, isOnboarded=$isOnboarded, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; Map toJson() { final json = {}; json[r'accessToken'] = this.accessToken; json[r'isAdmin'] = this.isAdmin; + json[r'isOnboarded'] = this.isOnboarded; json[r'name'] = this.name; json[r'profileImagePath'] = this.profileImagePath; json[r'shouldChangePassword'] = this.shouldChangePassword; @@ -83,6 +89,7 @@ class LoginResponseDto { return LoginResponseDto( accessToken: mapValueOfType(json, r'accessToken')!, isAdmin: mapValueOfType(json, r'isAdmin')!, + isOnboarded: mapValueOfType(json, r'isOnboarded')!, name: mapValueOfType(json, r'name')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, @@ -137,6 +144,7 @@ class LoginResponseDto { static const requiredKeys = { 'accessToken', 'isAdmin', + 'isOnboarded', 'name', 'profileImagePath', 'shouldChangePassword', diff --git a/mobile/openapi/lib/model/onboarding_dto.dart b/mobile/openapi/lib/model/onboarding_dto.dart new file mode 100644 index 00000000000..670b6a5c681 --- /dev/null +++ b/mobile/openapi/lib/model/onboarding_dto.dart @@ -0,0 +1,99 @@ +// +// 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 OnboardingDto { + /// Returns a new [OnboardingDto] instance. + OnboardingDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is OnboardingDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'OnboardingDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [OnboardingDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static OnboardingDto? fromJson(dynamic value) { + upgradeDto(value, "OnboardingDto"); + if (value is Map) { + final json = value.cast(); + + return OnboardingDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + 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 = OnboardingDto.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 = OnboardingDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of OnboardingDto-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] = OnboardingDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/onboarding_response_dto.dart b/mobile/openapi/lib/model/onboarding_response_dto.dart new file mode 100644 index 00000000000..033466e96b4 --- /dev/null +++ b/mobile/openapi/lib/model/onboarding_response_dto.dart @@ -0,0 +1,99 @@ +// +// 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 OnboardingResponseDto { + /// Returns a new [OnboardingResponseDto] instance. + OnboardingResponseDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is OnboardingResponseDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'OnboardingResponseDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [OnboardingResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static OnboardingResponseDto? fromJson(dynamic value) { + upgradeDto(value, "OnboardingResponseDto"); + if (value is Map) { + final json = value.cast(); + + return OnboardingResponseDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + 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 = OnboardingResponseDto.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 = OnboardingResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of OnboardingResponseDto-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] = OnboardingResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/server_apk_links_dto.dart b/mobile/openapi/lib/model/server_apk_links_dto.dart new file mode 100644 index 00000000000..086a2f172b3 --- /dev/null +++ b/mobile/openapi/lib/model/server_apk_links_dto.dart @@ -0,0 +1,123 @@ +// +// 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 ServerApkLinksDto { + /// Returns a new [ServerApkLinksDto] instance. + ServerApkLinksDto({ + required this.arm64v8a, + required this.armeabiv7a, + required this.universal, + required this.x8664, + }); + + String arm64v8a; + + String armeabiv7a; + + String universal; + + String x8664; + + @override + bool operator ==(Object other) => identical(this, other) || other is ServerApkLinksDto && + other.arm64v8a == arm64v8a && + other.armeabiv7a == armeabiv7a && + other.universal == universal && + other.x8664 == x8664; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (arm64v8a.hashCode) + + (armeabiv7a.hashCode) + + (universal.hashCode) + + (x8664.hashCode); + + @override + String toString() => 'ServerApkLinksDto[arm64v8a=$arm64v8a, armeabiv7a=$armeabiv7a, universal=$universal, x8664=$x8664]'; + + Map toJson() { + final json = {}; + json[r'arm64v8a'] = this.arm64v8a; + json[r'armeabiv7a'] = this.armeabiv7a; + json[r'universal'] = this.universal; + json[r'x86_64'] = this.x8664; + return json; + } + + /// Returns a new [ServerApkLinksDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ServerApkLinksDto? fromJson(dynamic value) { + upgradeDto(value, "ServerApkLinksDto"); + if (value is Map) { + final json = value.cast(); + + return ServerApkLinksDto( + arm64v8a: mapValueOfType(json, r'arm64v8a')!, + armeabiv7a: mapValueOfType(json, r'armeabiv7a')!, + universal: mapValueOfType(json, r'universal')!, + x8664: mapValueOfType(json, r'x86_64')!, + ); + } + 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 = ServerApkLinksDto.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 = ServerApkLinksDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ServerApkLinksDto-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] = ServerApkLinksDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'arm64v8a', + 'armeabiv7a', + 'universal', + 'x86_64', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index f5d59b6ae9d..a3aa7365adb 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -20,6 +20,7 @@ class SyncAssetV1 { required this.id, required this.isFavorite, required this.localDateTime, + required this.originalFileName, required this.ownerId, required this.thumbhash, required this.type, @@ -40,6 +41,8 @@ class SyncAssetV1 { DateTime? localDateTime; + String originalFileName; + String ownerId; String? thumbhash; @@ -57,6 +60,7 @@ class SyncAssetV1 { other.id == id && other.isFavorite == isFavorite && other.localDateTime == localDateTime && + other.originalFileName == originalFileName && other.ownerId == ownerId && other.thumbhash == thumbhash && other.type == type && @@ -72,13 +76,14 @@ class SyncAssetV1 { (id.hashCode) + (isFavorite.hashCode) + (localDateTime == null ? 0 : localDateTime!.hashCode) + + (originalFileName.hashCode) + (ownerId.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + (visibility.hashCode); @override - String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; + String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; Map toJson() { final json = {}; @@ -105,6 +110,7 @@ class SyncAssetV1 { } else { // json[r'localDateTime'] = null; } + json[r'originalFileName'] = this.originalFileName; json[r'ownerId'] = this.ownerId; if (this.thumbhash != null) { json[r'thumbhash'] = this.thumbhash; @@ -132,6 +138,7 @@ class SyncAssetV1 { id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, localDateTime: mapDateTime(json, r'localDateTime', r''), + originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, thumbhash: mapValueOfType(json, r'thumbhash'), type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!, @@ -190,6 +197,7 @@ class SyncAssetV1 { 'id', 'isFavorite', 'localDateTime', + 'originalFileName', 'ownerId', 'thumbhash', 'type', diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 215e691cb1e..c729e0d80fd 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class UserPreferencesResponseDto { /// Returns a new [UserPreferencesResponseDto] instance. UserPreferencesResponseDto({ + required this.cast, required this.download, required this.emailNotifications, required this.folders, @@ -24,6 +25,8 @@ class UserPreferencesResponseDto { required this.tags, }); + CastResponse cast; + DownloadResponse download; EmailNotificationsResponse emailNotifications; @@ -44,6 +47,7 @@ class UserPreferencesResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -57,6 +61,7 @@ class UserPreferencesResponseDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (cast.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + (folders.hashCode) + @@ -68,10 +73,11 @@ class UserPreferencesResponseDto { (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesResponseDto[cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; + json[r'cast'] = this.cast; json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; json[r'folders'] = this.folders; @@ -93,6 +99,7 @@ class UserPreferencesResponseDto { final json = value.cast(); return UserPreferencesResponseDto( + cast: CastResponse.fromJson(json[r'cast'])!, download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, folders: FoldersResponse.fromJson(json[r'folders'])!, @@ -149,6 +156,7 @@ class UserPreferencesResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'cast', 'download', 'emailNotifications', 'folders', diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 3e420df119a..73e3cac9ffd 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -14,6 +14,7 @@ class UserPreferencesUpdateDto { /// Returns a new [UserPreferencesUpdateDto] instance. UserPreferencesUpdateDto({ this.avatar, + this.cast, this.download, this.emailNotifications, this.folders, @@ -33,6 +34,14 @@ class UserPreferencesUpdateDto { /// AvatarUpdate? avatar; + /// + /// 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. + /// + CastUpdate? cast; + /// /// 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 @@ -108,6 +117,7 @@ class UserPreferencesUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -122,6 +132,7 @@ class UserPreferencesUpdateDto { int get hashCode => // ignore: unnecessary_parenthesis (avatar == null ? 0 : avatar!.hashCode) + + (cast == null ? 0 : cast!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + (folders == null ? 0 : folders!.hashCode) + @@ -133,7 +144,7 @@ class UserPreferencesUpdateDto { (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -142,6 +153,11 @@ class UserPreferencesUpdateDto { } else { // json[r'avatar'] = null; } + if (this.cast != null) { + json[r'cast'] = this.cast; + } else { + // json[r'cast'] = null; + } if (this.download != null) { json[r'download'] = this.download; } else { @@ -200,6 +216,7 @@ class UserPreferencesUpdateDto { return UserPreferencesUpdateDto( avatar: AvatarUpdate.fromJson(json[r'avatar']), + cast: CastUpdate.fromJson(json[r'cast']), download: DownloadUpdate.fromJson(json[r'download']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), folders: FoldersUpdate.fromJson(json[r'folders']), diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart new file mode 100644 index 00000000000..b8a7500d6e5 --- /dev/null +++ b/mobile/pigeon/native_sync_api.dart @@ -0,0 +1,89 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/native_sync_api.g.dart', + swiftOut: 'ios/Runner/Sync/Messages.g.swift', + swiftOptions: SwiftOptions(), + kotlinOut: + 'android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.sync'), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +class PlatformAsset { + final String id; + final String name; + // Follows AssetType enum from base_asset.model.dart + final int type; + // Seconds since epoch + final int? createdAt; + final int? updatedAt; + final int durationInSeconds; + + const PlatformAsset({ + required this.id, + required this.name, + required this.type, + this.createdAt, + this.updatedAt, + this.durationInSeconds = 0, + }); +} + +class PlatformAlbum { + final String id; + final String name; + // Seconds since epoch + final int? updatedAt; + final bool isCloud; + final int assetCount; + + const PlatformAlbum({ + required this.id, + required this.name, + this.updatedAt, + this.isCloud = false, + this.assetCount = 0, + }); +} + +class SyncDelta { + final bool hasChanges; + final List updates; + final List deletes; + // Asset -> Album mapping + final Map> assetAlbums; + + const SyncDelta({ + this.hasChanges = false, + this.updates = const [], + this.deletes = const [], + this.assetAlbums = const {}, + }); +} + +@HostApi() +abstract class NativeSyncApi { + bool shouldFullSync(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + SyncDelta getMediaChanges(); + + void checkpointSync(); + + void clearSyncCheckpoint(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAssetIdsForAlbum(String albumId); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAlbums(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + int getAssetsCountSince(String albumId, int timestamp); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAssetsForAlbum(String albumId, {int? updatedTimeCond}); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3df4e4e8a98..5c54a2c349d 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: - dependency: "direct overridden" + dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" analyzer_plugin: - dependency: "direct overridden" + dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.0" ansicolor: dependency: transitive description: @@ -74,10 +69,10 @@ packages: dependency: "direct dev" description: name: auto_route_generator - sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 + sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46 url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.3.1" background_downloader: dependency: "direct main" description: @@ -322,34 +317,42 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" + sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" + sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.0" dartx: dependency: transitive description: @@ -723,10 +726,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -971,10 +974,11 @@ packages: isar_generator: dependency: "direct dev" description: - name: isar_generator - sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1" - url: "https://pub.isar-community.dev" - source: hosted + path: "packages/isar_generator" + ref: v3 + resolved-ref: ad574f60ed6f39d2995cd16fc7dc3de9a646ef30 + url: "https://github.com/immich-app/isar" + source: git version: "3.1.8" js: dependency: transitive @@ -1072,14 +1076,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" maplibre_gl: dependency: "direct main" description: @@ -1121,7 +1117,7 @@ packages: source: hosted version: "0.11.1" meta: - dependency: "direct overridden" + dependency: transitive description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c @@ -1352,6 +1348,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: a093af76026160bb5ff6eb98e3e678a301ffd1001ac0d90be558bc133a0c73f5 + url: "https://pub.dev" + source: hosted + version: "25.3.2" pinput: dependency: "direct main" description: @@ -1361,7 +1365,7 @@ packages: source: hosted version: "5.0.1" platform: - dependency: transitive + dependency: "direct main" description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" @@ -1444,10 +1448,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.10" riverpod_annotation: dependency: "direct main" description: @@ -1460,18 +1464,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.5" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" + sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.5" rxdart: dependency: transitive description: @@ -1633,10 +1637,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_span: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6dd81b7fc19..81249fdcfa0 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: flutter_displaymode: ^0.6.0 flutter_hooks: ^0.21.2 flutter_local_notifications: ^17.2.1+2 + flutter_secure_storage: ^9.2.4 flutter_svg: ^2.0.17 flutter_udid: ^3.0.0 flutter_web_auth_2: ^5.0.0-alpha.0 @@ -41,6 +42,7 @@ dependencies: http: ^1.3.0 image_picker: ^1.1.2 intl: ^0.19.0 + local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.21.0 network_info_plus: ^6.1.3 @@ -52,6 +54,8 @@ dependencies: permission_handler: ^11.4.0 photo_manager: ^3.6.4 photo_manager_image_provider: ^2.2.0 + pinput: ^5.0.1 + platform: ^3.1.6 punycode: ^1.0.0 riverpod_annotation: ^2.6.1 scrollable_positioned_list: ^0.3.8 @@ -64,9 +68,6 @@ dependencies: uuid: ^4.5.1 wakelock_plus: ^1.2.10 worker_manager: ^7.2.3 - local_auth: ^2.3.0 - pinput: ^5.0.1 - flutter_secure_storage: ^9.2.4 native_video_player: git: @@ -84,11 +85,6 @@ dependencies: drift: ^2.23.1 drift_flutter: ^0.2.4 -dependency_overrides: - analyzer: ^6.0.0 - meta: ^1.11.0 - analyzer_plugin: ^0.11.3 - dev_dependencies: flutter_test: sdk: flutter @@ -98,11 +94,13 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 flutter_native_splash: ^2.4.5 isar_generator: - version: *isar_version - hosted: https://pub.isar-community.dev/ + git: + url: https://github.com/immich-app/isar + ref: v3 + path: packages/isar_generator/ integration_test: sdk: flutter - custom_lint: ^0.6.4 + custom_lint: ^0.7.5 riverpod_lint: ^2.6.1 riverpod_generator: ^2.6.1 mocktail: ^1.0.4 @@ -112,6 +110,8 @@ dev_dependencies: file: ^7.0.1 # for MemoryFileSystem # Drift generator drift_dev: ^2.23.1 + # Type safe platform code + pigeon: ^25.3.1 flutter: uses-material-design: true 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 index bfb61ef4029..df59f03c569 100644 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ b/mobile/test/modules/album/album_sort_by_options_provider_test.dart @@ -1,10 +1,10 @@ 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:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; @@ -225,6 +225,18 @@ void main() { 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', () { @@ -298,6 +310,8 @@ void main() { late AppSettingsService settingsMock; late ProviderContainer container; + registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse); + setUp(() async { settingsMock = MockAppSettingsService(); container = TestUtils.createContainer( @@ -305,6 +319,18 @@ void main() { 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', () { diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 98382a382ca..286fa47c660 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -5275,6 +5275,38 @@ ] } }, + "/server/apk-links": { + "get": { + "operationId": "getApkLinks", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerApkLinksDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Server" + ] + } + }, "/server/config": { "get": { "operationId": "getServerConfig", @@ -7890,6 +7922,101 @@ ] } }, + "/users/me/onboarding": { + "delete": { + "operationId": "deleteUserOnboarding", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Users" + ] + }, + "get": { + "operationId": "getUserOnboarding", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Users" + ] + }, + "put": { + "operationId": "setUserOnboarding", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Users" + ] + } + }, "/users/me/preferences": { "get": { "operationId": "getMyPreferences", @@ -8303,10 +8430,6 @@ "type": "array" } }, - "required": [ - "name", - "permissions" - ], "type": "object" }, "ActivityCreateDto": { @@ -9556,6 +9679,26 @@ ], "type": "string" }, + "CastResponse": { + "properties": { + "gCastEnabled": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "gCastEnabled" + ], + "type": "object" + }, + "CastUpdate": { + "properties": { + "gCastEnabled": { + "type": "boolean" + } + }, + "type": "object" + }, "ChangePasswordDto": { "properties": { "newPassword": { @@ -10356,6 +10499,9 @@ "isAdmin": { "type": "boolean" }, + "isOnboarded": { + "type": "boolean" + }, "name": { "type": "string" }, @@ -10375,6 +10521,7 @@ "required": [ "accessToken", "isAdmin", + "isOnboarded", "name", "profileImagePath", "shouldChangePassword", @@ -11019,6 +11166,28 @@ ], "type": "object" }, + "OnboardingDto": { + "properties": { + "isOnboarded": { + "type": "boolean" + } + }, + "required": [ + "isOnboarded" + ], + "type": "object" + }, + "OnboardingResponseDto": { + "properties": { + "isOnboarded": { + "type": "boolean" + } + }, + "required": [ + "isOnboarded" + ], + "type": "object" + }, "PartnerDirection": { "enum": [ "shared-by", @@ -11939,6 +12108,29 @@ ], "type": "object" }, + "ServerApkLinksDto": { + "properties": { + "arm64v8a": { + "type": "string" + }, + "armeabiv7a": { + "type": "string" + }, + "universal": { + "type": "string" + }, + "x86_64": { + "type": "string" + } + }, + "required": [ + "arm64v8a", + "armeabiv7a", + "universal", + "x86_64" + ], + "type": "object" + }, "ServerConfigDto": { "properties": { "externalDomain": { @@ -13056,6 +13248,9 @@ "nullable": true, "type": "string" }, + "originalFileName": { + "type": "string" + }, "ownerId": { "type": "string" }, @@ -13090,6 +13285,7 @@ "id", "isFavorite", "localDateTime", + "originalFileName", "ownerId", "thumbhash", "type", @@ -14806,6 +15002,9 @@ }, "UserPreferencesResponseDto": { "properties": { + "cast": { + "$ref": "#/components/schemas/CastResponse" + }, "download": { "$ref": "#/components/schemas/DownloadResponse" }, @@ -14835,6 +15034,7 @@ } }, "required": [ + "cast", "download", "emailNotifications", "folders", @@ -14852,6 +15052,9 @@ "avatar": { "$ref": "#/components/schemas/AvatarUpdate" }, + "cast": { + "$ref": "#/components/schemas/CastUpdate" + }, "download": { "$ref": "#/components/schemas/DownloadUpdate" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0ce6f417b15..e3e12dc56ec 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -128,6 +128,9 @@ export type UserAdminUpdateDto = { shouldChangePassword?: boolean; storageLabel?: string | null; }; +export type CastResponse = { + gCastEnabled: boolean; +}; export type DownloadResponse = { archiveSize: number; includeEmbeddedVideos: boolean; @@ -164,6 +167,7 @@ export type TagsResponse = { sidebarWeb: boolean; }; export type UserPreferencesResponseDto = { + cast: CastResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; folders: FoldersResponse; @@ -177,6 +181,9 @@ export type UserPreferencesResponseDto = { export type AvatarUpdate = { color?: UserAvatarColor; }; +export type CastUpdate = { + gCastEnabled?: boolean; +}; export type DownloadUpdate = { archiveSize?: number; includeEmbeddedVideos?: boolean; @@ -214,6 +221,7 @@ export type TagsUpdate = { }; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; + cast?: CastUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; folders?: FoldersUpdate; @@ -407,8 +415,8 @@ export type ApiKeyCreateResponseDto = { secret: string; }; export type ApiKeyUpdateDto = { - name: string; - permissions: Permission[]; + name?: string; + permissions?: Permission[]; }; export type AssetBulkDeleteDto = { force?: boolean; @@ -504,6 +512,7 @@ export type LoginCredentialDto = { export type LoginResponseDto = { accessToken: string; isAdmin: boolean; + isOnboarded: boolean; name: string; profileImagePath: string; shouldChangePassword: boolean; @@ -996,6 +1005,12 @@ export type ServerAboutResponseDto = { version: string; versionUrl: string; }; +export type ServerApkLinksDto = { + arm64v8a: string; + armeabiv7a: string; + universal: string; + x86_64: string; +}; export type ServerConfigDto = { externalDomain: string; isInitialized: boolean; @@ -1456,6 +1471,12 @@ export type UserUpdateMeDto = { name?: string; password?: string; }; +export type OnboardingResponseDto = { + isOnboarded: boolean; +}; +export type OnboardingDto = { + isOnboarded: boolean; +}; export type CreateProfileImageDto = { file: Blob; }; @@ -2860,6 +2881,14 @@ export function getAboutInfo(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getApkLinks(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerApkLinksDto; + }>("/server/apk-links", { + ...opts + })); +} export function getServerConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3560,6 +3589,32 @@ export function setUserLicense({ licenseKeyDto }: { body: licenseKeyDto }))); } +export function deleteUserOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/users/me/onboarding", { + ...opts, + method: "DELETE" + })); +} +export function getUserOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: OnboardingResponseDto; + }>("/users/me/onboarding", { + ...opts + })); +} +export function setUserOnboarding({ onboardingDto }: { + onboardingDto: OnboardingDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: OnboardingResponseDto; + }>("/users/me/onboarding", oazapfts.json({ + ...opts, + method: "PUT", + body: onboardingDto + }))); +} export function getMyPreferences(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/package-lock.json b/server/package-lock.json index 47ee93cae8c..592ce857f91 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -86,7 +86,7 @@ "@types/bcrypt": "^5.0.0", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", - "@types/express": "^4.17.17", + "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.21", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.197", @@ -2349,12 +2349,12 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.1.tgz", - "integrity": "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.2.tgz", + "integrity": "sha512-cHh4OPH44PjaHM93D1jgE1HO/B7XTZVRDxy/cPuGgyMEA4p2zXO+qqcOgTMC5FYcp7dX9jLeCjXAU0ToFAnODw==", "license": "MIT", "dependencies": { - "file-type": "20.5.0", + "file-type": "21.0.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", @@ -2380,9 +2380,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.1.tgz", - "integrity": "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.2.tgz", + "integrity": "sha512-QRuyxwu0BjNfmmmunsw1ylX7RSyfDQHt+xD+tKncdtgiMOOzAu+LA1gB4WoZnw4frQkk+qZbhEbM61cIjOxD3w==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2454,14 +2454,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.1.tgz", - "integrity": "sha512-IUxk380qnUtz0PCRQ5i+o9UHlGMrFzGPIJxDwyt3JZZwx2AngOlcEcm5e+7YeJQEr2QYX2QyC4tUQg0zde+D7A==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.2.tgz", + "integrity": "sha512-GlNwOT4htRp8RpZ+TpqGtSHwGKw/abdxxBRse40XE2SWs5ikaoujr9Yd+5sJWDNXB4QTftwb+FplXhyk1Ra+4A==", "license": "MIT", "dependencies": { "cors": "2.8.5", "express": "5.1.0", - "multer": "1.4.5-lts.2", + "multer": "2.0.0", "path-to-regexp": "8.2.0", "tslib": "2.8.1" }, @@ -2475,9 +2475,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.1.tgz", - "integrity": "sha512-Bsc8ouysUFasWiO8RKEvppqYM5LNkHfbyIJQTy3V6+PUdYhblkvmOq8QtjuHpv6DiBI4siUcxACx/90/CdXLkQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.2.tgz", + "integrity": "sha512-IkeDPRRddY0In6lE+5H/DJodtF5cEx+ga+GWehs4Il5Y3kK7MVR2/WgUABAhyRsbJYOhIhZD7Dai0V2t9ref1Q==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -2663,9 +2663,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.1.tgz", - "integrity": "sha512-stzm8YrLDGAijHYQw+8Z9dD6lGdvahL0hIjGVZ/0KBxLZht0/rvRjgV31UK+DUqXaF7yhJTw9ryrPaITxI1J6A==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.2.tgz", + "integrity": "sha512-BQxVKUVW6gzEbbHAvmg5RgcP3s++pRgTCmsgaDF/DtcLRUeKi8SjAdqzLm14xbkMeibxOf3fNqM2iwqUKj8ffw==", "dev": true, "license": "MIT", "dependencies": { @@ -2691,9 +2691,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.1.tgz", - "integrity": "sha512-gxwQoGx5bW5IvparzrX1UOGXz87eqY0fK5Y6yb14z6tSSubQTciNjCDm5osDEkRyRCG6ZB0F+eXF6dRUjwTlBQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.2.tgz", + "integrity": "sha512-Ywl7u0C3+qnKIrk0mD3jHWnowO+GScFT1FeP6cNgarA0ujHEfusph9IIbnUJiEiusfnKVpK9fYMGZRSDwnRGPQ==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -5342,22 +5342,21 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", - "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", + "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "dev": true, "license": "MIT", "dependencies": { @@ -5577,9 +5576,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", - "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", + "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5736,17 +5735,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -5760,7 +5759,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -5776,16 +5775,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -5800,15 +5799,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5818,15 +5818,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -5843,9 +5878,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -5857,14 +5892,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5910,16 +5947,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5934,13 +5971,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -9573,18 +9610,18 @@ } }, "node_modules/file-type": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", - "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -10202,9 +10239,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -12126,9 +12163,9 @@ } }, "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-bS8rPZurbAuHGAnApbM9d4h1wSoYqrOqkE+6a64KLMK9yWU7gJXBDDVklKQ3TPi9DRb85cRs6yXaC0+cjxRtRg==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -12140,7 +12177,7 @@ "xtend": "^4.0.0" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 10.16.0" } }, "node_modules/multer/node_modules/concat-stream": { @@ -17053,15 +17090,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17380,9 +17417,9 @@ } }, "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "license": "MIT", "engines": { "node": ">= 0.10" diff --git a/server/package.json b/server/package.json index 1aeafbf7cf9..ffd8876a1c8 100644 --- a/server/package.json +++ b/server/package.json @@ -112,7 +112,7 @@ "@types/bcrypt": "^5.0.0", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", - "@types/express": "^4.17.17", + "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.21", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.197", diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 267fc42ef4a..3544fce2a0b 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -3,6 +3,7 @@ import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, + ServerApkLinksDto, ServerConfigDto, ServerFeaturesDto, ServerMediaTypesResponseDto, @@ -34,6 +35,12 @@ export class ServerController { return this.service.getAboutInfo(); } + @Get('apk-links') + @Authenticated() + getApkLinks(): ServerApkLinksDto { + return this.service.getApkLinks(); + } + @Get('storage') @Authenticated() getStorage(): Promise { diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index f1bdf160d38..6c6eae15ff6 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -17,6 +17,7 @@ import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; +import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; @@ -87,6 +88,24 @@ export class UserController { await this.service.deleteLicense(auth); } + @Get('me/onboarding') + @Authenticated() + getUserOnboarding(@Auth() auth: AuthDto): Promise { + return this.service.getOnboarding(auth); + } + + @Put('me/onboarding') + @Authenticated() + async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise { + return this.service.setOnboarding(auth, Onboarding); + } + + @Delete('me/onboarding') + @Authenticated() + async deleteUserOnboarding(@Auth() auth: AuthDto): Promise { + await this.service.deleteOnboarding(auth); + } + @Get(':id') @Authenticated() getUser(@Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/database.ts b/server/src/database.ts index cfccd70b750..4e40e5c241e 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -341,6 +341,7 @@ export const columns = { syncAsset: [ 'id', 'ownerId', + 'originalFileName', 'thumbhash', 'checksum', 'fileCreatedAt', diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index ac6dd25bcf5..c790ea613dd 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -15,14 +15,16 @@ export class APIKeyCreateDto { } export class APIKeyUpdateDto { + @Optional() @IsString() @IsNotEmpty() - name!: string; + name?: string; + @Optional() @IsEnum(Permission, { each: true }) @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) @ArrayMinSize(1) - permissions!: Permission[]; + permissions?: Permission[]; } export class APIKeyCreateResponseDto { diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 2f3ae5c14bc..e94818b2b5d 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -2,7 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; -import { ImmichCookie } from 'src/enum'; +import { ImmichCookie, UserMetadataKey } from 'src/enum'; +import { UserMetadataItem } from 'src/types'; import { Optional, PinCode, toEmail } from 'src/validation'; export type CookieResponse = { @@ -39,9 +40,14 @@ export class LoginResponseDto { profileImagePath!: string; isAdmin!: boolean; shouldChangePassword!: boolean; + isOnboarded!: boolean; } export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { + const onboardingMetadata = entity.metadata.find( + (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + )?.value; + return { accessToken, userId: entity.id, @@ -50,6 +56,7 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR isAdmin: entity.isAdmin, profileImagePath: entity.profileImagePath, shouldChangePassword: entity.shouldChangePassword, + isOnboarded: onboardingMetadata?.isOnboarded ?? false, }; } diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts new file mode 100644 index 00000000000..0028fca006e --- /dev/null +++ b/server/src/dtos/onboarding.dto.ts @@ -0,0 +1,9 @@ +import { IsBoolean, IsNotEmpty } from 'class-validator'; + +export class OnboardingDto { + @IsBoolean() + @IsNotEmpty() + isOnboarded!: boolean; +} + +export class OnboardingResponseDto extends OnboardingDto {} diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index e1f94dbaa55..47442ad4fbc 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -37,6 +37,13 @@ export class ServerAboutResponseDto { thirdPartySupportUrl?: string; } +export class ServerApkLinksDto { + arm64v8a!: string; + armeabiv7a!: string; + universal!: string; + x86_64!: string; +} + export class ServerStorageResponseDto { diskSize!: string; diskUse!: string; diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 0043cfb40b4..3e50762c8a8 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -59,6 +59,7 @@ export class SyncPartnerDeleteV1 { export class SyncAssetV1 { id!: string; ownerId!: string; + originalFileName!: string; thumbhash!: string | null; checksum!: string; fileCreatedAt!: Date | null; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index a9d32523aeb..43e15689b93 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -85,6 +85,11 @@ class PurchaseUpdate { hideBuyButtonUntil?: string; } +class CastUpdate { + @ValidateBoolean({ optional: true }) + gCastEnabled?: boolean; +} + export class UserPreferencesUpdateDto { @Optional() @ValidateNested() @@ -135,6 +140,11 @@ export class UserPreferencesUpdateDto { @ValidateNested() @Type(() => PurchaseUpdate) purchase?: PurchaseUpdate; + + @Optional() + @ValidateNested() + @Type(() => CastUpdate) + cast?: CastUpdate; } class RatingsResponse { @@ -183,6 +193,10 @@ class PurchaseResponse { hideBuyButtonUntil!: string; } +class CastResponse { + gCastEnabled: boolean = false; +} + export class UserPreferencesResponseDto implements UserPreferences { folders!: FoldersResponse; memories!: MemoriesResponse; @@ -193,6 +207,7 @@ export class UserPreferencesResponseDto implements UserPreferences { emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; purchase!: PurchaseResponse; + cast!: CastResponse; } export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { diff --git a/server/src/enum.ts b/server/src/enum.ts index b00b0133937..e7e40eb122a 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -211,6 +211,7 @@ export enum SystemMetadataKey { export enum UserMetadataKey { PREFERENCES = 'preferences', LICENSE = 'license', + ONBOARDING = 'onboarding', } export enum UserAvatarColor { diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index 23539de642a..2323d407636 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -88,7 +88,8 @@ export class FileUploadInterceptor implements NestInterceptor { if (handler) { await new Promise((resolve, reject) => { const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve()); - handler(context_.getRequest(), context_.getResponse(), next); + const maybePromise = handler(context_.getRequest(), context_.getResponse(), next); + Promise.resolve(maybePromise).catch((error) => reject(error)); }); } else { this.logger.warn(`Skipping invalid file upload route: ${route}`); diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 3040de8e03f..3db4ee6f5b0 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -73,3 +73,4 @@ where and "activity"."albumId" = $2 and "activity"."isLiked" = $3 and "assets"."deletedAt" is null + and "assets"."visibility" != 'locked' diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 2b351368eff..26ddccbe176 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -80,6 +80,7 @@ select where "albums_assets_assets"."albumsId" = "albums"."id" and "assets"."deletedAt" is null + and "assets"."visibility" in ('archive', 'timeline') order by "assets"."fileCreatedAt" desc ) as "asset" @@ -178,7 +179,8 @@ from "assets" inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id" where - "album_assets"."albumsId" in ($1) + "assets"."visibility" in ('archive', 'timeline') + and "album_assets"."albumsId" in ($1) and "assets"."deletedAt" is null group by "album_assets"."albumsId" diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 3d47b7517e6..5caf2e30cd5 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -186,8 +186,8 @@ from inner join "smart_search" on "assets"."id" = "smart_search"."assetId" inner join "asset_job_status" as "job_status" on "job_status"."assetId" = "assets"."id" where - "assets"."visibility" != $1 - and "assets"."deletedAt" is null + "assets"."deletedAt" is null + and "assets"."visibility" in ('archive', 'timeline') and "job_status"."duplicatesDetectedAt" is null -- AssetJobRepository.streamForEncodeClip @@ -349,7 +349,7 @@ from "assets" as "stacked" where "stacked"."deletedAt" is not null - and "stacked"."visibility" != $1 + and "stacked"."visibility" = $1 and "stacked"."stackId" = "asset_stack"."id" group by "asset_stack"."id" diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 8efaa6a17b4..d85ad341d00 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -130,7 +130,6 @@ select from "assets" left join "exif" on "assets"."id" = "exif"."assetId" - left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" where "assets"."id" = any ($1::uuid[]) @@ -240,10 +239,7 @@ with "assets" where "assets"."deletedAt" is null - and ( - "assets"."visibility" = $1 - or "assets"."visibility" = $2 - ) + and "assets"."visibility" in ('archive', 'timeline') ) select "timeBucket", @@ -300,21 +296,14 @@ with where "stacked"."stackId" = "assets"."stackId" and "stacked"."deletedAt" is null - and "stacked"."visibility" != $1 + and "stacked"."visibility" = $1 group by "stacked"."stackId" ) as "stacked_assets" on true where "assets"."deletedAt" is null - and ( - "assets"."visibility" = $2 - or "assets"."visibility" = $3 - ) - and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4 - and ( - "assets"."visibility" = $5 - or "assets"."visibility" = $6 - ) + and "assets"."visibility" in ('archive', 'timeline') + and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $2 and not exists ( select from @@ -374,10 +363,10 @@ with "exif"."assetId" = "assets"."id" ) as "asset" on true where - "assets"."ownerId" = $1::uuid + "assets"."visibility" in ('archive', 'timeline') + and "assets"."ownerId" = $1::uuid and "assets"."duplicateId" is not null and "assets"."deletedAt" is null - and "assets"."visibility" != $2 and "assets"."stackId" is null group by "assets"."duplicateId" @@ -388,12 +377,12 @@ with from "duplicates" where - json_array_length("assets") = $3 + json_array_length("assets") = $2 ), "removed_unique" as ( update "assets" set - "duplicateId" = $4 + "duplicateId" = $3 from "unique" where diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 48854f48720..b8da3b5ae3a 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -182,27 +182,42 @@ from "asset_faces" left join "assets" on "assets"."id" = "asset_faces"."assetId" and "asset_faces"."personId" = $1 - and "assets"."visibility" != $2 + and "assets"."visibility" = 'timeline' and "assets"."deletedAt" is null where "asset_faces"."deletedAt" is null -- PersonRepository.getNumberOfPeople select - count(distinct ("person"."id")) as "total", - count(distinct ("person"."id")) filter ( - where - "person"."isHidden" = $1 + coalesce(count(*), 0) as "total", + coalesce( + count(*) filter ( + where + "isHidden" = $1 + ), + 0 ) as "hidden" from "person" - inner join "asset_faces" on "asset_faces"."personId" = "person"."id" - inner join "assets" on "assets"."id" = "asset_faces"."assetId" - and "assets"."deletedAt" is null - and "assets"."visibility" != $2 where - "person"."ownerId" = $3 - and "asset_faces"."deletedAt" is null + exists ( + select + from + "asset_faces" + where + "asset_faces"."personId" = "person"."id" + and "asset_faces"."deletedAt" is null + and exists ( + select + from + "assets" + where + "assets"."id" = "asset_faces"."assetId" + and "assets"."visibility" = 'timeline' + and "assets"."deletedAt" is null + ) + ) + and "person"."ownerId" = $2 -- PersonRepository.refreshFaces with diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index c100089179a..806fdb1c701 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -102,23 +102,23 @@ with "assets" inner join "smart_search" on "assets"."id" = "smart_search"."assetId" where - "assets"."ownerId" = any ($2::uuid[]) + "assets"."visibility" in ('archive', 'timeline') + and "assets"."ownerId" = any ($2::uuid[]) and "assets"."deletedAt" is null - and "assets"."visibility" != $3 - and "assets"."type" = $4 - and "assets"."id" != $5::uuid + and "assets"."type" = $3 + and "assets"."id" != $4::uuid and "assets"."stackId" is null order by "distance" limit - $6 + $5 ) select * from "cte" where - "cte"."distance" <= $7 + "cte"."distance" <= $6 commit -- SearchRepository.searchFaces @@ -241,7 +241,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "state" is not null @@ -253,7 +253,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "city" is not null @@ -265,7 +265,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "make" is not null @@ -277,6 +277,6 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "model" is not null diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 1f0c9401018..6d450cd435b 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -52,6 +52,7 @@ select where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" + and "assets"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from @@ -135,6 +136,7 @@ select where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" + and "assets"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index f797f5c0b52..f9565b6effa 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -76,6 +76,7 @@ order by select "id", "ownerId", + "originalFileName", "thumbhash", "checksum", "fileCreatedAt", @@ -98,6 +99,7 @@ order by select "id", "ownerId", + "originalFileName", "thumbhash", "checksum", "fileCreatedAt", diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 33f29602663..0638b8c965d 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -290,7 +290,7 @@ order by select "users"."id" as "userId", "users"."name" as "userName", - "users"."quotaSizeInBytes" as "quotaSizeInBytes", + "users"."quotaSizeInBytes", count(*) filter ( where ( @@ -335,9 +335,8 @@ select from "users" left join "assets" on "assets"."ownerId" = "users"."id" + and "assets"."deletedAt" is null left join "exif" on "exif"."assetId" = "assets"."id" -where - "assets"."deletedAt" is null group by "users"."id" order by diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index d030a99f4f0..f8bbfdf8a65 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -5,6 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { Activity, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetVisibility } from 'src/enum'; import { asUuid } from 'src/utils/database'; export interface ActivitySearch { @@ -76,6 +77,7 @@ export class ActivityRepository { .where('activity.albumId', '=', albumId) .where('activity.isLiked', '=', false) .where('assets.deletedAt', 'is', null) + .where('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED)) .executeTakeFirstOrThrow(); return count; diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index c8bdae6d31a..7131a72f611 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -6,6 +6,7 @@ import { columns, Exif } from 'src/database'; import { Albums, DB } from 'src/db'; import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { AlbumUserCreateDto } from 'src/dtos/album.dto'; +import { withDefaultVisibility } from 'src/utils/database'; export interface AlbumAssetCount { albumId: string; @@ -58,6 +59,7 @@ const withAssets = (eb: ExpressionBuilder) => { .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') .where('assets.deletedAt', 'is', null) + .$call(withDefaultVisibility) .orderBy('assets.fileCreatedAt', 'desc') .as('asset'), ) @@ -121,6 +123,7 @@ export class AlbumRepository { return ( this.db .selectFrom('assets') + .$call(withDefaultVisibility) .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id') .select('album_assets.albumsId as albumId') .select((eb) => eb.fn.min(sql`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate')) diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 6f86edaaa18..d629202f043 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -11,6 +11,7 @@ import { anyUuid, asUuid, toJson, + withDefaultVisibility, withExif, withExifInner, withFaces, @@ -140,9 +141,9 @@ export class AssetJobRepository { return this.db .selectFrom('assets') .select(['assets.id']) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) .where('assets.deletedAt', 'is', null) .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .$call(withDefaultVisibility) .$if(!force, (qb) => qb .innerJoin('asset_job_status as job_status', 'job_status.assetId', 'assets.id') @@ -226,7 +227,7 @@ export class AssetJobRepository { .select(['asset_stack.id', 'asset_stack.primaryAssetId']) .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) .where('stacked.deletedAt', 'is not', null) - .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) + .where('stacked.visibility', '=', AssetVisibility.TIMELINE) .whereRef('stacked.stackId', '=', 'asset_stack.id') .groupBy('asset_stack.id') .as('stacked_assets'), diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 60744ddc5f6..416cf4e5de9 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -300,7 +300,6 @@ export class AssetRepository { .select(withFacesAndPeople) .select(withTags) .$call(withExif) - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') .where('assets.id', '=', anyUuid(ids)) .execute(); } @@ -523,8 +522,8 @@ export class AssetRepository { .selectFrom('assets') .selectAll('assets') .$call(withExif) + .$call(withDefaultVisibility) .where('ownerId', '=', anyUuid(userIds)) - .where('visibility', '!=', AssetVisibility.HIDDEN) .where('deletedAt', 'is', null) .orderBy((eb) => eb.fn('random')) .limit(take) @@ -634,8 +633,6 @@ export class AssetRepository { ) .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(options.visibility == undefined, withDefaultVisibility) - .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) .$if(!!options.withStacked, (qb) => qb @@ -656,7 +653,7 @@ export class AssetRepository { .select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack')) .whereRef('stacked.stackId', '=', 'assets.stackId') .where('stacked.deletedAt', 'is', null) - .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) + .where('stacked.visibility', '=', AssetVisibility.TIMELINE) .groupBy('stacked.stackId') .as('stacked_assets'), (join) => join.onTrue(), @@ -709,6 +706,7 @@ export class AssetRepository { .with('duplicates', (qb) => qb .selectFrom('assets') + .$call(withDefaultVisibility) .leftJoinLateral( (qb) => qb @@ -727,7 +725,6 @@ export class AssetRepository { .where('assets.duplicateId', 'is not', null) .$narrowType<{ duplicateId: NotNull }>() .where('assets.deletedAt', 'is', null) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) .where('assets.stackId', 'is', null) .groupBy('assets.duplicateId'), ) diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 1e8e147c430..94d9029f60c 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -246,6 +246,7 @@ export class DatabaseRepository { return; } const dimSize = await this.getDimensionSize(table); + lists ||= this.targetListCount(await this.getRowCount(table)); await this.db.schema.dropIndex(indexName).ifExists().execute(); if (table === 'smart_search') { await this.db.schema.alterTable(table).dropConstraint('dim_size_constraint').ifExists().execute(); @@ -262,7 +263,6 @@ export class DatabaseRepository { ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE ${sql.raw(schema)}vector(${sql.raw(String(dimSize))})`.execute(tx); - lists ||= this.targetListCount(await this.getRowCount(table)); await sql.raw(vectorIndexQuery({ vectorExtension, table, indexName, lists })).execute(tx); }); try { diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index ea9f0b1901a..357b52a77af 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -40,8 +40,8 @@ export class OAuthRepository { redirect_uri: redirectUrl, scope: config.scope, state, - code_challenge: codeChallenge, - code_challenge_method: 'S256', + code_challenge: client.serverMetadata().supportsPKCE() ? codeChallenge : '', + code_challenge_method: client.serverMetadata().supportsPKCE() ? 'S256' : '', }).toString(); return { url, state, codeVerifier }; } diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 70a99802019..229a523c17a 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -38,11 +38,6 @@ export interface PersonStatistics { assets: number; } -export interface PeopleStatistics { - total: number; - hidden: number; -} - export interface DeleteFacesOptions { sourceType: SourceType; } @@ -151,7 +146,7 @@ export class PersonRepository { .innerJoin('assets', (join) => join .onRef('asset_faces.assetId', '=', 'assets.id') - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) + .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('assets.deletedAt', 'is', null), ) .where('person.ownerId', '=', userId) @@ -341,7 +336,7 @@ export class PersonRepository { join .onRef('assets.id', '=', 'asset_faces.assetId') .on('asset_faces.personId', '=', personId) - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) + .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('assets.deletedAt', 'is', null), ) .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count')) @@ -354,35 +349,31 @@ export class PersonRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - async getNumberOfPeople(userId: string): Promise { - const items = await this.db + getNumberOfPeople(userId: string) { + const zero = sql.lit(0); + return this.db .selectFrom('person') - .innerJoin('asset_faces', 'asset_faces.personId', 'person.id') + .where((eb) => + eb.exists((eb) => + eb + .selectFrom('asset_faces') + .whereRef('asset_faces.personId', '=', 'person.id') + .where('asset_faces.deletedAt', 'is', null) + .where((eb) => + eb.exists((eb) => + eb + .selectFrom('assets') + .whereRef('assets.id', '=', 'asset_faces.assetId') + .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('assets.deletedAt', 'is', null), + ), + ), + ), + ) .where('person.ownerId', '=', userId) - .where('asset_faces.deletedAt', 'is', null) - .innerJoin('assets', (join) => - join - .onRef('assets.id', '=', 'asset_faces.assetId') - .on('assets.deletedAt', 'is', null) - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE), - ) - .select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total')) - .select((eb) => - eb.fn - .count(eb.fn('distinct', ['person.id'])) - .filterWhere('person.isHidden', '=', true) - .as('hidden'), - ) - .executeTakeFirst(); - - if (items == undefined) { - return { total: 0, hidden: 0 }; - } - - return { - total: Number(items.total), - hidden: Number(items.hidden), - }; + .select((eb) => eb.fn.coalesce(eb.fn.countAll(), zero).as('total')) + .select((eb) => eb.fn.coalesce(eb.fn.countAll().filterWhere('isHidden', '=', true), zero).as('hidden')) + .executeTakeFirstOrThrow(); } create(person: Insertable) { diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index a7b7027b7bd..747a59c65b6 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -7,7 +7,7 @@ import { DummyValue, GenerateSql } from 'src/decorators'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum'; import { probes } from 'src/repositories/database.repository'; -import { anyUuid, asUuid, searchAssetBuilder } from 'src/utils/database'; +import { anyUuid, asUuid, searchAssetBuilder, withDefaultVisibility } from 'src/utils/database'; import { paginationHelper } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; @@ -268,6 +268,7 @@ export class SearchRepository { .with('cte', (qb) => qb .selectFrom('assets') + .$call(withDefaultVisibility) .select([ 'assets.id as assetId', 'assets.duplicateId', @@ -276,7 +277,6 @@ export class SearchRepository { .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') .where('assets.ownerId', '=', anyUuid(userIds)) .where('assets.deletedAt', 'is', null) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) .where('assets.type', '=', type) .where('assets.id', '!=', asUuid(assetId)) .where('assets.stackId', 'is', null) @@ -472,7 +472,7 @@ export class SearchRepository { .distinctOn(field) .innerJoin('assets', 'assets.id', 'exif.assetId') .where('ownerId', '=', anyUuid(userIds)) - .where('visibility', '!=', AssetVisibility.HIDDEN) + .where('visibility', '=', AssetVisibility.TIMELINE) .where('deletedAt', 'is', null) .where(field, 'is not', null); } diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index c9d69fb37f4..78ff255579c 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { AssetStack, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { asUuid } from 'src/utils/database'; +import { asUuid, withDefaultVisibility } from 'src/utils/database'; export interface StackSearch { ownerId: string; @@ -34,7 +34,8 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) ) .select((eb) => eb.fn.toJson('exifInfo').as('exifInfo')) .where('assets.deletedAt', 'is', null) - .whereRef('assets.stackId', '=', 'asset_stack.id'), + .whereRef('assets.stackId', '=', 'asset_stack.id') + .$call(withDefaultVisibility), ).as('assets'); }; diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 6972479df6c..06159041c5b 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -210,9 +210,9 @@ export class UserRepository { getUserStats() { return this.db .selectFrom('users') - .leftJoin('assets', 'assets.ownerId', 'users.id') + .leftJoin('assets', (join) => join.onRef('assets.ownerId', '=', 'users.id').on('assets.deletedAt', 'is', null)) .leftJoin('exif', 'exif.assetId', 'assets.id') - .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes as quotaSizeInBytes']) + .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes']) .select((eb) => [ eb.fn .countAll() @@ -256,7 +256,6 @@ export class UserRepository { ) .as('usageVideos'), ]) - .where('assets.deletedAt', 'is', null) .groupBy('users.id') .orderBy('users.createdAt', 'asc') .execute(); diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index c2b792d0918..f3bb7d1d5c1 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -210,6 +210,17 @@ describe(AlbumService.name, () => { false, ); }); + + it('should throw an error if the userId is the ownerId', async () => { + mocks.user.get.mockResolvedValue(userStub.admin); + await expect( + sut.create(authStub.admin, { + albumName: 'Empty album', + albumUsers: [{ userId: userStub.admin.id, role: AlbumUserRole.EDITOR }], + }), + ).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.album.create).not.toHaveBeenCalled(); + }); }); describe('update', () => { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index d4e6ab7ffd1..83d95355054 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -93,6 +93,10 @@ export class AlbumService extends BaseService { if (!exists) { throw new BadRequestException('User not found'); } + + if (userId == auth.user.id) { + throw new BadRequestException('Cannot share album with owner'); + } } const allowedAssetIdsSet = await this.checkAccess({ diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index bc73ff64107..55906bf0a68 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -17,11 +17,16 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; +import { requireElevatedPermission } from 'src/utils/access'; import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; @Injectable() export class AssetService extends BaseService { async getStatistics(auth: AuthDto, dto: AssetStatsDto) { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const stats = await this.assetRepository.getStatistics(auth.user.id, dto); return mapStats(stats); } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 4bc5f1ce0b0..a773f4a1cfc 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -28,6 +28,7 @@ const oauthResponse = ({ name, profileImagePath, isAdmin: false, + isOnboarded: false, shouldChangePassword: false, }); @@ -101,6 +102,7 @@ describe(AuthService.name, () => { name: user.name, profileImagePath: user.profileImagePath, isAdmin: user.isAdmin, + isOnboarded: false, shouldChangePassword: user.shouldChangePassword, }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index df286d18097..3f122b5e741 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -14,8 +14,9 @@ import { SearchSuggestionType, SmartSearchDto, } from 'src/dtos/search.dto'; -import { AssetOrder } from 'src/enum'; +import { AssetOrder, AssetVisibility } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { requireElevatedPermission } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { isSmartSearchEnabled } from 'src/utils/misc'; @@ -40,9 +41,11 @@ export class SearchService extends BaseService { } async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise { - let checksum: Buffer | undefined; - const userIds = await this.getUserIdsToSearch(auth); + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + let checksum: Buffer | undefined; if (dto.checksum) { const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; checksum = Buffer.from(dto.checksum, encoding); @@ -50,6 +53,7 @@ export class SearchService extends BaseService { const page = dto.page ?? 1; const size = dto.size || 250; + const userIds = await this.getUserIdsToSearch(auth); const { hasNextPage, items } = await this.searchRepository.searchMetadata( { page, size }, { @@ -64,12 +68,20 @@ export class SearchService extends BaseService { } async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const userIds = await this.getUserIdsToSearch(auth); const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds }); return items.map((item) => mapAsset(item, { auth })); } async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { throw new BadRequestException('Smart search is not enabled'); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 9112c40a17d..bada717f4a5 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -5,6 +5,7 @@ import { OnEvent } from 'src/decorators'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, + ServerApkLinksDto, ServerConfigDto, ServerFeaturesDto, ServerMediaTypesResponseDto, @@ -48,6 +49,16 @@ export class ServerService extends BaseService { }; } + getApkLinks(): ServerApkLinksDto { + const baseUrl = `https://github.com/immich-app/immich/releases/download/v${serverVersion.toString()}`; + return { + arm64v8a: `${baseUrl}/app-arm64-v8a-release.apk`, + armeabiv7a: `${baseUrl}/app-armeabi-v7a-release.apk`, + universal: `${baseUrl}/app-release.apk`, + x86_64: `${baseUrl}/app-x86_64-release.apk`, + }; + } + async getStorage(): Promise { const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase); diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index f3ebcc2cd72..abd536a97e2 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -4,6 +4,7 @@ import { TimeBucketAssetDto, TimeBucketDto, TimeBucketsResponseDto } from 'src/d import { AssetVisibility, Permission } from 'src/enum'; import { TimeBucketOptions } from 'src/repositories/asset.repository'; import { BaseService } from 'src/services/base.service'; +import { requireElevatedPermission } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; @Injectable() @@ -44,6 +45,10 @@ export class TimelineService extends BaseService { } private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + if (dto.albumId) { await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); } else { diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index a0304d51ad6..78f49fd7ae0 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -6,6 +6,7 @@ import { StorageCore } from 'src/cores/storage.core'; import { OnJob } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; +import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; @@ -179,6 +180,39 @@ export class UserService extends BaseService { return { ...license, activatedAt }; } + async getOnboarding(auth: AuthDto): Promise { + const metadata = await this.userRepository.getMetadata(auth.user.id); + + const onboardingData = metadata.find( + (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + )?.value; + + if (!onboardingData) { + return { isOnboarded: false }; + } + + return { + isOnboarded: onboardingData.isOnboarded, + }; + } + + async deleteOnboarding({ user }: AuthDto): Promise { + await this.userRepository.deleteMetadata(user.id, UserMetadataKey.ONBOARDING); + } + + async setOnboarding(auth: AuthDto, onboarding: OnboardingDto): Promise { + await this.userRepository.upsertMetadata(auth.user.id, { + key: UserMetadataKey.ONBOARDING, + value: { + isOnboarded: onboarding.isOnboarded, + }, + }); + + return { + isOnboarded: onboarding.isOnboarded, + }; + } + @OnJob({ name: JobName.USER_SYNC_USAGE, queue: QueueName.BACKGROUND_TASK }) async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage(); diff --git a/server/src/types.ts b/server/src/types.ts index d166a94e8b6..2e613c124e1 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -502,9 +502,13 @@ export interface UserPreferences { showSupportBadge: boolean; hideBuyButtonUntil: string; }; + cast: { + gCastEnabled: boolean; + }; } export interface UserMetadata extends Record> { [UserMetadataKey.PREFERENCES]: DeepPartial; [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string }; + [UserMetadataKey.ONBOARDING]: { isOnboarded: boolean }; } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 38697a654ba..c1b162927d4 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -304,3 +304,9 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe } } }; + +export const requireElevatedPermission = (auth: AuthDto) => { + if (!auth.session?.hasElevatedPermission) { + throw new UnauthorizedException('Elevated permission is required'); + } +}; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 40bf7503db4..5e5c6c5fb4c 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -153,17 +153,12 @@ export function toJson(qb: SelectQueryBuilder) { - return qb.where((qb) => - qb.or([ - qb('assets.visibility', '=', AssetVisibility.TIMELINE), - qb('assets.visibility', '=', AssetVisibility.ARCHIVE), - ]), - ); + return qb.where('assets.visibility', 'in', [sql.lit(AssetVisibility.ARCHIVE), sql.lit(AssetVisibility.TIMELINE)]); } +// TODO come up with a better query that only selects the fields we need export function withExif(qb: SelectQueryBuilder) { return qb .leftJoin('exif', 'assets.id', 'exif.assetId') diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index a013c0b74ed..009dabce58c 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -42,6 +42,9 @@ const getDefaultPreferences = (): UserPreferences => { showSupportBadge: true, hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), }, + cast: { + gCastEnabled: false, + }, }; }; diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index 3cf6d7d30dd..b46ccd97e1d 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -23,12 +23,14 @@ describe.concurrent(SyncEntityType.AssetV1, () => { it('should detect and sync the first asset', async () => { const { auth, sut, getRepository, testSync } = await setup(); + const originalFileName = 'firstAsset'; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); const assetRepo = getRepository('asset'); const asset = mediumFactory.assetInsert({ + originalFileName, ownerId: auth.user.id, checksum: Buffer.from(checksum, 'base64'), thumbhash: Buffer.from(thumbhash, 'base64'), @@ -48,6 +50,7 @@ describe.concurrent(SyncEntityType.AssetV1, () => { ack: expect.any(String), data: { id: asset.id, + originalFileName, ownerId: asset.ownerId, thumbhash, checksum, diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index 70e31eca4ce..125047b1bf1 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -23,6 +23,7 @@ describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { it('should detect and sync the first partner asset', async () => { const { auth, sut, getRepository, testSync } = await setup(); + const originalFileName = 'firstPartnerAsset'; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); @@ -34,6 +35,7 @@ describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { const assetRepo = getRepository('asset'); const asset = mediumFactory.assetInsert({ ownerId: user2.id, + originalFileName, checksum: Buffer.from(checksum, 'base64'), thumbhash: Buffer.from(thumbhash, 'base64'), fileCreatedAt: date, @@ -56,6 +58,7 @@ describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { data: { id: asset.id, ownerId: asset.ownerId, + originalFileName, thumbhash, checksum, deletedAt: null, diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 75e36c1da2e..b70f02bcf5d 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -15,8 +15,8 @@ import { } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserStatus } from 'src/enum'; -import { OnThisDayData } from 'src/types'; +import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; +import { OnThisDayData, UserMetadataItem } from 'src/types'; export const newUuid = () => randomUUID() as string; export const newUuids = () => @@ -146,6 +146,12 @@ const userFactory = (user: Partial = {}) => ({ avatarColor: null, profileImagePath: '', profileChangedAt: newDate(), + metadata: [ + { + key: UserMetadataKey.ONBOARDING, + value: 'true', + }, + ] as UserMetadataItem[], ...user, }); diff --git a/web/eslint.config.js b/web/eslint.config.js index 9ced6195044..9a545fcbc7d 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -29,7 +29,6 @@ export default typescriptEslint.config( '**/yarn.lock', '**/svelte.config.js', 'eslint.config.js', - 'postcss.config.cjs', 'tailwind.config.js', 'coverage', ], diff --git a/web/package-lock.json b/web/package-lock.json index a631c514ff3..10e4c0dc396 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -47,7 +47,6 @@ "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.8", @@ -68,7 +67,6 @@ "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", "globals": "^16.0.0", - "postcss": "^8.5.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", @@ -103,19 +101,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -2244,9 +2229,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", - "integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz", + "integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2256,13 +2241,13 @@ "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.7" + "tailwindcss": "4.1.8" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz", - "integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz", + "integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2274,24 +2259,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-x64": "4.1.7", - "@tailwindcss/oxide-freebsd-x64": "4.1.7", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-x64-musl": "4.1.7", - "@tailwindcss/oxide-wasm32-wasi": "4.1.7", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" + "@tailwindcss/oxide-android-arm64": "4.1.8", + "@tailwindcss/oxide-darwin-arm64": "4.1.8", + "@tailwindcss/oxide-darwin-x64": "4.1.8", + "@tailwindcss/oxide-freebsd-x64": "4.1.8", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", + "@tailwindcss/oxide-linux-x64-musl": "4.1.8", + "@tailwindcss/oxide-wasm32-wasi": "4.1.8", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz", - "integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz", + "integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==", "cpu": [ "arm64" ], @@ -2306,9 +2291,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz", - "integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz", + "integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==", "cpu": [ "arm64" ], @@ -2323,9 +2308,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz", - "integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz", + "integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==", "cpu": [ "x64" ], @@ -2340,9 +2325,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz", - "integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz", + "integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==", "cpu": [ "x64" ], @@ -2357,9 +2342,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz", - "integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz", + "integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==", "cpu": [ "arm" ], @@ -2374,9 +2359,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz", - "integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz", + "integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==", "cpu": [ "arm64" ], @@ -2391,9 +2376,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz", - "integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz", + "integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==", "cpu": [ "arm64" ], @@ -2408,9 +2393,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz", - "integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz", + "integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==", "cpu": [ "x64" ], @@ -2425,9 +2410,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz", - "integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz", + "integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==", "cpu": [ "x64" ], @@ -2442,9 +2427,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz", - "integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz", + "integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2463,7 +2448,7 @@ "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", + "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, @@ -2471,70 +2456,10 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.9", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", - "integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz", + "integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==", "cpu": [ "arm64" ], @@ -2549,9 +2474,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz", - "integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz", + "integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==", "cpu": [ "x64" ], @@ -2642,30 +2567,16 @@ "node": ">=18" } }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.7.tgz", - "integrity": "sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", - "postcss": "^8.4.41", - "tailwindcss": "4.1.7" - } - }, "node_modules/@tailwindcss/vite": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz", - "integrity": "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.8.tgz", + "integrity": "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", - "tailwindcss": "4.1.7" + "@tailwindcss/node": "4.1.8", + "@tailwindcss/oxide": "4.1.8", + "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" @@ -2977,17 +2888,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3001,7 +2912,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -3017,16 +2928,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -3041,15 +2952,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3059,15 +2971,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3084,9 +3031,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -3098,14 +3045,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3151,16 +3100,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3175,13 +3124,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5008,9 +4957,9 @@ } }, "node_modules/fabric": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.5.tgz", - "integrity": "sha512-BFxyLDeLMMgtteqQwKAyRM+oSkf82lDFzsiC7AMob7k7ag7naFuHOtWtcll4v+M9Cpn5aqRBfz1shnsO0vZhbg==", + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.6.tgz", + "integrity": "sha512-cL0m/RanEIiP67/TAj8kAQcEYlXofeB1SXCB1w7a0ktyUQHdRpnm2/VHlqsD/PfSLlGqftHzmxAS4LvKzSlrEw==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -5480,9 +5429,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -8663,9 +8612,9 @@ } }, "node_modules/svelte": { - "version": "5.33.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.1.tgz", - "integrity": "sha512-7znzaaQALL62NBzkdKV04tmYIVla8qjrW+k6GdgFZcKcj8XOb8iEjmfRPo40iaWZlKv3+uiuc0h4iaGgwoORtA==", + "version": "5.33.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.6.tgz", + "integrity": "sha512-bxg2QY03JlrilCZmDlshY95Argj0rnX43UQFWZN4fct8PZTNBBmvfow2A6yOW1+YweDjhC2qdZF66ASI0Y21Tw==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -8914,9 +8863,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", - "integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", + "integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==", "license": "MIT" }, "node_modules/tapable": { @@ -9240,15 +9189,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/web/package.json b/web/package.json index b47b521e6a2..c1fd724704c 100644 --- a/web/package.json +++ b/web/package.json @@ -64,7 +64,6 @@ "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.8", @@ -85,7 +84,6 @@ "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", "globals": "^16.0.0", - "postcss": "^8.5.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs deleted file mode 100644 index e5640725a9e..00000000000 --- a/web/postcss.config.cjs +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts index 24a1a25ac1b..29074fc7b04 100644 --- a/web/src/lib/actions/zoom-image.ts +++ b/web/src/lib/actions/zoom-image.ts @@ -1,15 +1,12 @@ -import { photoZoomState, zoomed } from '$lib/stores/zoom-image.store'; +import { photoZoomState } from '$lib/stores/zoom-image.store'; import { useZoomImageWheel } from '@zoom-image/svelte'; import { get } from 'svelte/store'; -export { zoomed } from '$lib/stores/zoom-image.store'; - export const zoomImageAction = (node: HTMLElement) => { const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel(); createZoomImage(node, { maxZoom: 10, - wheelZoomRatio: 0.2, }); const state = get(photoZoomState); @@ -17,10 +14,7 @@ export const zoomImageAction = (node: HTMLElement) => { setZoomImageState(state); } - const unsubscribes = [ - zoomed.subscribe((state) => setZoomImageState({ currentZoom: state ? 2 : 1 })), - zoomImageState.subscribe((state) => photoZoomState.set(state)), - ]; + const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)]; return { destroy() { for (const unsubscribe of unsubscribes) { diff --git a/web/src/lib/cast/cast-button.svelte b/web/src/lib/cast/cast-button.svelte index c6be1c11d72..392418daa5b 100644 --- a/web/src/lib/cast/cast-button.svelte +++ b/web/src/lib/cast/cast-button.svelte @@ -2,44 +2,23 @@ import { t } from 'svelte-i18n'; import { onMount } from 'svelte'; import { mdiCast, mdiCastConnected } from '@mdi/js'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { CastDestinationType, castManager } from '$lib/managers/cast-manager.svelte'; import { GCastDestination } from '$lib/utils/cast/gcast-destination.svelte'; import { IconButton } from '@immich/ui'; - interface Props { - whiteHover?: boolean; - navBar?: boolean; - } - - let { whiteHover, navBar }: Props = $props(); - onMount(async () => { await castManager.initialize(); }); - - const getButtonColor = () => { - return castManager.isCasting ? 'primary' : whiteHover ? undefined : 'opaque'; - }; {#if castManager.availableDestinations.length > 0 && castManager.availableDestinations[0].type === CastDestinationType.GCAST} - {#if navBar} - void GCastDestination.showCastDialog()} - aria-label={$t('cast')} - /> - {:else} - - {/if} + void GCastDestination.showCastDialog()} + aria-label={$t('cast')} + /> {/if} diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index c77ff60f22b..e85232deaab 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -1,6 +1,5 @@ - + {#if albumMapViewManager.isInMapView} diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 9fbcf4e2ad2..3a20e106022 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -167,7 +167,7 @@ {$t('role_editor')} {/if} {#if user.id !== album.ownerId} - + {#if role === AlbumUserRole.Viewer} handleUpdateSharedUserRole(user, AlbumUserRole.Editor)} diff --git a/web/src/lib/components/album-page/album-shared-link.svelte b/web/src/lib/components/album-page/album-shared-link.svelte index b56aa11b6d2..e7d6503da3c 100644 --- a/web/src/lib/components/album-page/album-shared-link.svelte +++ b/web/src/lib/components/album-page/album-shared-link.svelte @@ -1,9 +1,8 @@ - onClick(!isPlaying)} /> diff --git a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte index dff470f456d..ab974275241 100644 --- a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte @@ -56,6 +56,6 @@ toggleLockedVisibility()} - text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')} + text={isLocked ? $t('move_off_locked_folder') : $t('move_to_locked_folder')} icon={isLocked ? mdiLockOpenVariantOutline : mdiLockOutline} /> diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte index 7e2ffa1b943..5ab60fcb4c5 100644 --- a/web/src/lib/components/asset-viewer/actions/share-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte @@ -1,10 +1,10 @@ - + diff --git a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte index 5f6ac715db8..8ac087bca6e 100644 --- a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte @@ -1,6 +1,6 @@ - (isOpen = true) }} /> + {#if isOwner && !authManager.key}
@@ -75,16 +68,10 @@ type="button" class="rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 flex place-items-center place-content-center gap-1 px-2 py-1" title="Add tag" - onclick={handleAdd} + onclick={handleAddTag} > Add
{/if} - -{#if isOpen} - - handleTag(tagsIds)} onCancel={handleCancel} /> - -{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 4d0c164ae14..de8e355d33b 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -27,6 +27,7 @@ type AssetResponseDto, type ExifResponseDto, } from '@immich/sdk'; + import { IconButton } from '@immich/ui'; import { mdiCalendar, mdiCameraIris, @@ -42,7 +43,6 @@ import { t } from 'svelte-i18n'; import { slide } from 'svelte/transition'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import PersonSidePanel from '../faces-page/person-side-panel.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte'; @@ -158,7 +158,14 @@
- +

{$t('info')}

@@ -193,30 +200,34 @@

{$t('people').toUpperCase()}

{#if people.some((person) => person.isHidden)} - (showingHiddenPeople = !showingHiddenPeople)} /> {/if} - (isFaceEditMode.value = !isFaceEditMode.value)} /> {#if people.length > 0 || unassignedFaces.length > 0} - (showEditFaces = true)} /> {/if} @@ -369,11 +380,13 @@

{asset.originalFileName} {#if isOwner} - {/if} diff --git a/web/src/lib/components/asset-viewer/download-panel.svelte b/web/src/lib/components/asset-viewer/download-panel.svelte index b4e128e2f4d..2eafaf72ed7 100644 --- a/web/src/lib/components/asset-viewer/download-panel.svelte +++ b/web/src/lib/components/asset-viewer/download-panel.svelte @@ -5,7 +5,7 @@ import { t } from 'svelte-i18n'; import { fly, slide } from 'svelte/transition'; import { getByteUnitString } from '../../utils/byte-units'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; + import { IconButton } from '@immich/ui'; const abort = (downloadKey: string, download: DownloadProgress) => { download.abort?.abort(); @@ -42,10 +42,13 @@

- abort(downloadKey, download)} - size="20" + size="large" icon={mdiClose} class="dark:text-immich-dark-gray" /> diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte index daeccd90743..13e662ac329 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte @@ -1,5 +1,4 @@ @@ -113,6 +114,17 @@ { shortcut: { key: 'Escape' }, onShortcut: onClose }, { shortcut: { key: 'ArrowLeft' }, onShortcut: onPrevious }, { shortcut: { key: 'ArrowRight' }, onShortcut: onNext }, + { + shortcut: { key: ' ' }, + onShortcut: () => { + if (progressBarStatus === ProgressBarStatus.Paused) { + progressBar?.play(); + } else { + progressBar?.pause(); + } + }, + preventDefault: true, + }, ]} /> @@ -120,7 +132,7 @@ {#if showControls}
(isOverControls = true)} onmouseleave={() => (isOverControls = false)} transition:fly={{ duration: 150 }} @@ -133,7 +145,6 @@ icon={mdiClose} onclick={onClose} aria-label={$t('exit_slideshow')} - class="text-white" /> (progressBarStatus === ProgressBarStatus.Paused ? progressBar?.play() : progressBar?.pause())} aria-label={progressBarStatus === ProgressBarStatus.Paused ? $t('play') : $t('pause')} - class="text-white" /> {#if !isFullScreen} {/if}
{/if}
{:else} - handlePlayPauseButton()} - title={castManager.castState == CastState.PLAYING ? 'Pause' : 'Play'} + aria-label={castManager.castState == CastState.PLAYING ? 'Pause' : 'Play'} /> {/if} diff --git a/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts b/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts deleted file mode 100644 index eef4508c4e9..00000000000 --- a/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; -import { render, screen } from '@testing-library/svelte'; - -describe('CircleIconButton component', () => { - it('should render as a button', () => { - render(CircleIconButton, { icon: '', title: 'test' }); - const button = screen.getByRole('button'); - expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('type', 'button'); - expect(button).not.toHaveAttribute('href'); - expect(button).toHaveAttribute('title', 'test'); - }); - - it('should render as a link if href prop is set', () => { - render(CircleIconButton, { props: { href: '/test', icon: '', title: 'test' } }); - const link = screen.getByRole('link'); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', '/test'); - expect(link).not.toHaveAttribute('type'); - }); - - it('should render icon inside button', () => { - render(CircleIconButton, { icon: '', title: 'test' }); - const button = screen.getByRole('button'); - const icon = button.querySelector('svg'); - expect(icon).toBeInTheDocument(); - expect(icon).toHaveAttribute('aria-label', 'test'); - }); -}); diff --git a/web/src/lib/components/elements/buttons/circle-icon-button.svelte b/web/src/lib/components/elements/buttons/circle-icon-button.svelte deleted file mode 100644 index c243c06f923..00000000000 --- a/web/src/lib/components/elements/buttons/circle-icon-button.svelte +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - diff --git a/web/src/lib/components/elements/search-bar.svelte b/web/src/lib/components/elements/search-bar.svelte index c852be3b68b..24402857046 100644 --- a/web/src/lib/components/elements/search-bar.svelte +++ b/web/src/lib/components/elements/search-bar.svelte @@ -1,9 +1,9 @@ -{#if addImportPath} - { - addImportPath = false; - importPathToAdd = null; - }} - /> -{/if} - -{#if editImportPath != undefined} - (editImportPath = null)} - /> -{/if} -
@@ -199,15 +187,13 @@ @@ -221,7 +207,7 @@ {/if} diff --git a/web/src/lib/components/forms/library-scan-settings-form.svelte b/web/src/lib/components/forms/library-scan-settings-form.svelte index 137cfa1277e..639771f8a9a 100644 --- a/web/src/lib/components/forms/library-scan-settings-form.svelte +++ b/web/src/lib/components/forms/library-scan-settings-form.svelte @@ -1,12 +1,12 @@ -{#if addExclusionPattern} - (addExclusionPattern = false)} - /> -{/if} - -{#if editExclusionPattern != undefined} - (editExclusionPattern = null)} - /> -{/if} -
{validatedPath.importPath} - { - editImportPath = listIndex; - editedImportPath = validatedPath.importPath; - }} + aria-label={$t('edit_import_path')} + onclick={() => onEditImportPath(listIndex)} + size="small" />
- +
@@ -131,15 +116,14 @@ > @@ -153,13 +137,9 @@ {/if} diff --git a/web/src/lib/components/layouts/ErrorLayout.svelte b/web/src/lib/components/layouts/ErrorLayout.svelte index dbf3b136bea..f2f59457f7d 100644 --- a/web/src/lib/components/layouts/ErrorLayout.svelte +++ b/web/src/lib/components/layouts/ErrorLayout.svelte @@ -1,8 +1,8 @@
{/if} {@render children?.()} + +
+ {#if previousTitle} +
+ +
+ {/if} + +
+ +
+
diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte index 70df619ae0b..f1b1516bbe5 100644 --- a/web/src/lib/components/onboarding-page/onboarding-hello.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-hello.svelte @@ -1,28 +1,21 @@ - - -

+

+ +

{$t('onboarding_welcome_user', { values: { user: $user.name } })}

-

{$t('onboarding_welcome_description')}

- -
- -
- +

+ {userRole == OnboardingRole.SERVER + ? $t('onboarding_server_welcome_description') + : $t('onboarding_user_welcome_description')} +

+
diff --git a/web/src/lib/components/onboarding-page/onboarding-language.svelte b/web/src/lib/components/onboarding-page/onboarding-language.svelte new file mode 100644 index 00000000000..a37b026f130 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-language.svelte @@ -0,0 +1,12 @@ + + +
+

+ {$t('onboarding_locale_description')} +

+ + +
diff --git a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte deleted file mode 100644 index 12f4084fbc3..00000000000 --- a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - -

- {$t('onboarding_privacy_description')} -

- - {#if config && $user} - - {#if config} - - -
-
- -
-
- -
-
- {/if} -
- {/if} -
diff --git a/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte new file mode 100644 index 00000000000..a4af880fb4b --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte @@ -0,0 +1,35 @@ + + +
+

+ {$t('onboarding_privacy_description')} +

+ + {#if $systemConfig} + + + {/if} +
diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index de2ce7e9801..baa45779e55 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -5,18 +5,7 @@ import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { getConfig, type SystemConfigDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; - import { mdiArrowLeft, mdiCheck, mdiHarddisk } from '@mdi/js'; import { onMount } from 'svelte'; - import { t } from 'svelte-i18n'; - import OnboardingCard from './onboarding-card.svelte'; - - interface Props { - onDone: () => void; - onPrevious: () => void; - } - - let { onDone, onPrevious }: Props = $props(); let config: SystemConfigDto | undefined = $state(); let adminSettingsComponent = $state>(); @@ -24,9 +13,13 @@ onMount(async () => { config = await getConfig(); }); + + export const save = async () => { + await adminSettingsComponent?.handleSave({ storageTemplate: config?.storageTemplate }); + }; - +

{#snippet children({ message })} @@ -48,36 +41,9 @@ onSave={(config) => adminSettingsComponent?.handleSave(config)} onReset={(options) => adminSettingsComponent?.handleReset(options)} duration={0} - > -

-
- -
-
- -
-
- + /> {/if} {/snippet} {/if} - +
diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte index b128a9755c5..26e8fd9c7ac 100644 --- a/web/src/lib/components/onboarding-page/onboarding-theme.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-theme.svelte @@ -3,27 +3,16 @@ import Icon from '$lib/components/elements/icon.svelte'; import { Theme } from '$lib/constants'; import { themeManager } from '$lib/managers/theme-manager.svelte'; - import { Button } from '@immich/ui'; - import { mdiArrowRight, mdiThemeLightDark } from '@mdi/js'; import { t } from 'svelte-i18n'; - import OnboardingCard from './onboarding-card.svelte'; - - interface Props { - onDone: () => void; - } - - let { onDone }: Props = $props(); - -
-

{$t('onboarding_theme_description')}

-
+
+

{$t('onboarding_theme_description')}

-
+
- -
-
- -
-
- +
diff --git a/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte new file mode 100644 index 00000000000..d65ade1b184 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte @@ -0,0 +1,32 @@ + + +
+

+ {$t('onboarding_privacy_description')} +

+ + +
diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte index 8e8311a2ed6..e36038972f4 100644 --- a/web/src/lib/components/photos-page/actions/archive-action.svelte +++ b/web/src/lib/components/photos-page/actions/archive-action.svelte @@ -1,5 +1,4 @@ - + diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index 5cdcffb9371..b16e0465230 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -1,5 +1,4 @@ - + diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index 8fa73516093..f07bfd53ccc 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -1,9 +1,8 @@ - (isOpen = true) }} /> + {#if menuItem} - + {/if} {#if !menuItem} - {#if loading} - {}} /> - {:else} - - {/if} -{/if} - -{#if isOpen} - handleTag(tagIds)} onCancel={handleCancel} /> + {/if} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 72927891e4b..fd3dec5e75f 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -16,13 +16,13 @@ import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js'; import { t } from 'svelte-i18n'; import AssetViewer from '../asset-viewer/asset-viewer.svelte'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import DownloadAction from '../photos-page/actions/download-action.svelte'; import RemoveFromSharedLink from '../photos-page/actions/remove-from-shared-link.svelte'; import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte'; import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; + import { IconButton } from '@immich/ui'; interface Props { sharedLink: SharedLinkResponseDto; @@ -52,7 +52,7 @@ let results: (string | undefined)[] = []; results = await (!files || files.length === 0 || !Array.isArray(files) ? openFileUploadDialog() - : fileUploadHandler(files)); + : fileUploadHandler({ files })); const data = await addSharedLinkAssets({ id: sharedLink.id, assetIdsDto: { @@ -95,7 +95,14 @@ assets={assetInteraction.selectedAssets} clearSelect={() => cancelMultiselect(assetInteraction)} > - + {#if sharedLink?.allowDownload} {/if} @@ -111,15 +118,25 @@ {#snippet trailing()} {#if sharedLink?.allowUpload} - handleUploadAssets()} icon={mdiFileImagePlusOutline} /> {/if} {#if sharedLink?.allowDownload} - + {/if} {/snippet} diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index b6d32f20b0b..8d5800e9a8a 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -22,9 +22,9 @@ + +
+ {#if showSettingDescription} +
+
+ +
+ +

{$t('language_setting_description')}

+
+ {/if} + + value === closestLanguage) || defaultLangOption} + placeholder={$t('language')} + onSelect={(event) => handleLanguageChange(event?.value)} + options={langOptions} + /> +
diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index d247c9448dc..665c620563c 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -1,6 +1,5 @@ {#if !themeManager.theme.system} - themeManager.toggleTheme()} - {padding} + themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)} /> {/if} diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte index a0d4d250f7c..135dda0acae 100644 --- a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte +++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte @@ -1,6 +1,6 @@
@@ -103,14 +82,7 @@
- value === closestLanguage) || defaultLangOption} - options={langOptions} - title={$t('language')} - subtitle={$t('language_setting_description')} - onSelect={(combobox) => handleLanguageChange(combobox?.value)} - /> +
diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte index 47636fe4bf0..622de546294 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -1,8 +1,8 @@ - +
@@ -95,7 +99,7 @@
- +
diff --git a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte b/web/src/lib/modals/LibraryExclusionPatternModal.svelte similarity index 84% rename from web/src/lib/components/forms/library-exclusion-pattern-form.svelte rename to web/src/lib/modals/LibraryExclusionPatternModal.svelte index e069e5c7a27..d182a89684b 100644 --- a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte +++ b/web/src/lib/modals/LibraryExclusionPatternModal.svelte @@ -9,9 +9,7 @@ exclusionPatterns?: string[]; isEditing?: boolean; submitText?: string; - onCancel: () => void; - onSubmit: (exclusionPattern: string) => void; - onDelete?: () => void; + onClose: (data?: { action: 'delete' } | { action: 'submit'; exclusionPattern: string }) => void; } let { @@ -19,9 +17,7 @@ exclusionPatterns = $bindable([]), isEditing = false, submitText = $t('submit'), - onCancel, - onSubmit, - onDelete, + onClose, }: Props = $props(); onMount(() => { @@ -36,12 +32,12 @@ const onsubmit = (event: Event) => { event.preventDefault(); if (canSubmit) { - onSubmit(exclusionPattern); + onClose({ action: 'submit', exclusionPattern }); } }; - +

@@ -68,13 +64,15 @@

- + {#if isEditing} - + {/if} - +
diff --git a/web/src/lib/components/forms/library-import-path-form.svelte b/web/src/lib/modals/LibraryImportPathModal.svelte similarity index 82% rename from web/src/lib/components/forms/library-import-path-form.svelte rename to web/src/lib/modals/LibraryImportPathModal.svelte index ee2a273708c..56a5449fef8 100644 --- a/web/src/lib/components/forms/library-import-path-form.svelte +++ b/web/src/lib/modals/LibraryImportPathModal.svelte @@ -11,9 +11,7 @@ cancelText?: string; submitText?: string; isEditing?: boolean; - onCancel: () => void; - onSubmit: (importPath: string | null) => void; - onDelete?: () => void; + onClose: (data?: { action: 'delete' } | { action: 'submit'; importPath: string | null }) => void; } let { @@ -23,9 +21,7 @@ cancelText = $t('cancel'), submitText = $t('save'), isEditing = false, - onCancel, - onSubmit, - onDelete, + onClose, }: Props = $props(); onMount(() => { @@ -40,12 +36,12 @@ const onsubmit = (event: Event) => { event.preventDefault(); if (canSubmit) { - onSubmit(importPath); + onClose({ action: 'submit', importPath }); } }; - +

{$t('admin.library_import_path_description')}

@@ -65,13 +61,15 @@
- + {#if isEditing} - + {/if} - +
diff --git a/web/src/lib/components/forms/library-rename-form.svelte b/web/src/lib/modals/LibraryRenameModal.svelte similarity index 73% rename from web/src/lib/components/forms/library-rename-form.svelte rename to web/src/lib/modals/LibraryRenameModal.svelte index 0c04c77da10..9ba8048316e 100644 --- a/web/src/lib/components/forms/library-rename-form.svelte +++ b/web/src/lib/modals/LibraryRenameModal.svelte @@ -6,21 +6,20 @@ interface Props { library: Partial; - onCancel: () => void; - onSubmit: (library: Partial) => void; + onClose: (library?: Partial) => void; } - let { library, onCancel, onSubmit }: Props = $props(); + let { library, onClose }: Props = $props(); let newName = $state(library.name); const onsubmit = (event: Event) => { event.preventDefault(); - onSubmit({ ...library, name: newName }); + onClose({ ...library, name: newName }); }; - + @@ -31,7 +30,7 @@
- +
diff --git a/web/src/lib/components/forms/library-user-picker-form.svelte b/web/src/lib/modals/LibraryUserPickerModal.svelte similarity index 82% rename from web/src/lib/components/forms/library-user-picker-form.svelte rename to web/src/lib/modals/LibraryUserPickerModal.svelte index 43b3eb69f16..52cde12921a 100644 --- a/web/src/lib/components/forms/library-user-picker-form.svelte +++ b/web/src/lib/modals/LibraryUserPickerModal.svelte @@ -8,11 +8,10 @@ import { t } from 'svelte-i18n'; interface Props { - onCancel: () => void; - onSubmit: (ownerId: string) => void; + onClose: (ownerId?: string) => void; } - let { onCancel, onSubmit }: Props = $props(); + let { onClose }: Props = $props(); let ownerId: string = $state($user.id); @@ -25,11 +24,11 @@ const onsubmit = (event: Event) => { event.preventDefault(); - onSubmit(ownerId); + onClose(ownerId); }; - +

{$t('admin.note_cannot_be_changed_later')}

@@ -40,7 +39,7 @@
- +
diff --git a/web/src/lib/modals/PersonMergeSuggestionModal.svelte b/web/src/lib/modals/PersonMergeSuggestionModal.svelte index e762b30c037..decf2f37d1f 100644 --- a/web/src/lib/modals/PersonMergeSuggestionModal.svelte +++ b/web/src/lib/modals/PersonMergeSuggestionModal.svelte @@ -7,12 +7,11 @@ import { getPeopleThumbnailUrl } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { mergePerson, type PersonResponseDto } from '@immich/sdk'; - import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; + import { Button, IconButton, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiArrowLeft, mdiMerge } from '@mdi/js'; import { onMount, tick } from 'svelte'; import { t } from 'svelte-i18n'; import ImageThumbnail from '../components/assets/thumbnail/image-thumbnail.svelte'; - import CircleIconButton from '../components/elements/buttons/circle-icon-button.svelte'; interface Props { personToMerge: PersonResponseDto; @@ -75,8 +74,11 @@ />
- ([personToMerge, personToBeMergedInto] = [personToBeMergedInto, personToMerge])} /> diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/modals/SlideshowSettingsModal.svelte similarity index 86% rename from web/src/lib/components/slideshow-settings.svelte rename to web/src/lib/modals/SlideshowSettingsModal.svelte index 0e0019e3eb5..d97b86fb128 100644 --- a/web/src/lib/components/slideshow-settings.svelte +++ b/web/src/lib/modals/SlideshowSettingsModal.svelte @@ -12,17 +12,24 @@ mdiShuffle, } from '@mdi/js'; import { t } from 'svelte-i18n'; + import type { RenderedOption } from '../components/elements/dropdown.svelte'; + import SettingDropdown from '../components/shared-components/settings/setting-dropdown.svelte'; import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store'; - import type { RenderedOption } from './elements/dropdown.svelte'; - import SettingDropdown from './shared-components/settings/setting-dropdown.svelte'; - const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook, slideshowTransition } = slideshowStore; + const { + slideshowDelay, + showProgressBar, + slideshowNavigation, + slideshowLook, + slideshowTransition, + slideshowAutoplay, + } = slideshowStore; interface Props { - onClose?: () => void; + onClose: () => void; } - let { onClose = () => {} }: Props = $props(); + let { onClose }: Props = $props(); // Temporary variables to hold the settings - marked as reactive with $state() but initialized with store values let tempSlideshowDelay = $state($slideshowDelay); @@ -30,6 +37,7 @@ let tempSlideshowNavigation = $state($slideshowNavigation); let tempSlideshowLook = $state($slideshowLook); let tempSlideshowTransition = $state($slideshowTransition); + let tempSlideshowAutoplay = $state($slideshowAutoplay); const navigationOptions: Record = { [SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: $t('shuffle') }, @@ -60,6 +68,7 @@ $slideshowNavigation = tempSlideshowNavigation; $slideshowLook = tempSlideshowLook; $slideshowTransition = tempSlideshowTransition; + $slideshowAutoplay = tempSlideshowAutoplay; onClose(); }; @@ -83,6 +92,7 @@ tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook; }} /> + ({ publicUsers: true, }); +export type SystemConfig = SystemConfigDto & { loaded: boolean }; +export const systemConfig = writable(); + export const retrieveServerConfig = async () => { const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]); featureFlags.update(() => ({ ...flags, loaded: true })); serverConfig.update(() => ({ ...config, loaded: true })); }; + +export const retrieveSystemConfig = async () => { + const config = await getConfig(); + systemConfig.update(() => ({ ...config, loaded: true })); +}; diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index 5bfcd099cb8..48639f4669f 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -39,6 +39,7 @@ function createSlideshowStore() { const showProgressBar = persisted('slideshow-show-progressbar', true); const slideshowDelay = persisted('slideshow-delay', 5, {}); const slideshowTransition = persisted('slideshow-transition', true); + const slideshowAutoplay = persisted('slideshow-autoplay', true, {}); return { restartProgress: { @@ -69,6 +70,7 @@ function createSlideshowStore() { slideshowDelay, showProgressBar, slideshowTransition, + slideshowAutoplay, }; } diff --git a/web/src/lib/stores/zoom-image.store.ts b/web/src/lib/stores/zoom-image.store.ts index c31092c4f7a..2c6ee189727 100644 --- a/web/src/lib/stores/zoom-image.store.ts +++ b/web/src/lib/stores/zoom-image.store.ts @@ -2,4 +2,3 @@ import type { ZoomImageWheelState } from '@zoom-image/core'; import { writable } from 'svelte/store'; export const photoZoomState = writable(); -export const zoomed = writable(); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 645c485cc5a..bfb8998781a 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -275,6 +275,10 @@ export const oauth = { } return false; }, + isAutoLaunchEnabled: (location: Location) => { + const value = 'autoLaunch=1'; + return location.search.includes(value); + }, authorize: async (location: Location) => { const $t = get(t); try { diff --git a/web/src/lib/utils/cast/gcast-destination.svelte.ts b/web/src/lib/utils/cast/gcast-destination.svelte.ts index fcfb8c382a6..f101c504f04 100644 --- a/web/src/lib/utils/cast/gcast-destination.svelte.ts +++ b/web/src/lib/utils/cast/gcast-destination.svelte.ts @@ -1,6 +1,8 @@ import { CastDestinationType, CastState, type ICastDestination } from '$lib/managers/cast-manager.svelte'; +import { preferences } from '$lib/stores/user.store'; import 'chromecast-caf-sender'; import { Duration } from 'luxon'; +import { get } from 'svelte/store'; const FRAMEWORK_LINK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'; @@ -24,6 +26,12 @@ export class GCastDestination implements ICastDestination { private currentUrl: string | null = null; async initialize(): Promise { + const preferencesStore = get(preferences); + if (!preferencesStore.cast.gCastEnabled) { + this.isAvailable = false; + return false; + } + // this is a really messy way since google does a pseudo-callbak // in the form of a global window event. We will give Chrome 3 seconds to respond // or we will mark the destination as unavailable diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index db43e92f94f..c5dd3ebd8e4 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -7,6 +7,7 @@ import { ExecutorQueue } from '$lib/utils/executor-queue'; import { Action, AssetMediaStatus, + AssetVisibility, checkBulkUpload, getAssetOriginalPath, getBaseUrl, @@ -73,7 +74,7 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => { } const files = Array.from(target.files); - resolve(fileUploadHandler(files, albumId, assetId)); + resolve(fileUploadHandler({ files, albumId, replaceAssetId: assetId })); }); fileSelector.click(); @@ -84,11 +85,16 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => { }); }; -export const fileUploadHandler = async ( - files: File[], - albumId?: string, - replaceAssetId?: string, -): Promise => { +type FileUploadHandlerParams = Omit & { + files: File[]; +}; + +export const fileUploadHandler = async ({ + files, + albumId, + replaceAssetId, + isLockedAssets = false, +}: FileUploadHandlerParams): Promise => { const extensions = await getExtensions(); const promises = []; for (const file of files) { @@ -96,7 +102,11 @@ export const fileUploadHandler = async ( if (extensions.some((extension) => name.endsWith(extension))) { const deviceAssetId = getDeviceAssetId(file); uploadAssetsStore.addItem({ id: deviceAssetId, file, albumId }); - promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, deviceAssetId, albumId, replaceAssetId))); + promises.push( + uploadExecutionQueue.addTask(() => + fileUploader({ assetFile: file, deviceAssetId, albumId, replaceAssetId, isLockedAssets }), + ), + ); } } @@ -108,13 +118,22 @@ function getDeviceAssetId(asset: File) { return 'web' + '-' + asset.name + '-' + asset.lastModified; } +type FileUploaderParams = { + assetFile: File; + albumId?: string; + replaceAssetId?: string; + isLockedAssets?: boolean; + deviceAssetId: string; +}; + // TODO: should probably use the @api SDK -async function fileUploader( - assetFile: File, - deviceAssetId: string, - albumId?: string, - replaceAssetId?: string, -): Promise { +async function fileUploader({ + assetFile, + deviceAssetId, + albumId, + replaceAssetId, + isLockedAssets = false, +}: FileUploaderParams): Promise { const fileCreatedAt = new Date(assetFile.lastModified).toISOString(); const $t = get(t); @@ -134,6 +153,10 @@ async function fileUploader( formData.append(key, value); } + if (isLockedAssets) { + formData.append('visibility', AssetVisibility.Locked); + } + let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined; const key = authManager.key; if (crypto?.subtle?.digest && !key) { diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 41eb5ee73b8..c3b4d83f385 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -17,6 +17,7 @@ export const isSharedLinkRoute = (route?: string | null) => !!route?.startsWith( export const isSearchRoute = (route?: string | null) => !!route?.startsWith('/(user)/search'); export const isAlbumsRoute = (route?: string | null) => !!route?.startsWith('/(user)/albums/[albumId=id]'); export const isPeopleRoute = (route?: string | null) => !!route?.startsWith('/(user)/people/[personId]'); +export const isLockedFolderRoute = (route?: string | null) => !!route?.startsWith('/(user)/locked'); export const isAssetViewerRoute = (target?: NavigationTarget | null) => !!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {})); diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0d2f9d79e31..bf870f8b0c2 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -9,7 +9,6 @@ import AlbumTitle from '$lib/components/album-page/album-title.svelte'; import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte'; import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; @@ -44,6 +43,7 @@ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets-store.svelte'; + import { featureFlags } from '$lib/stores/server-config.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { preferences, user } from '$lib/stores/user.store'; import { handlePromiseError, makeSharedLinkUrl } from '$lib/utils'; @@ -69,7 +69,7 @@ updateAlbumInfo, type AlbumUserAddDto, } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, IconButton } from '@immich/ui'; import { mdiArrowLeft, mdiCogOutline, @@ -493,10 +493,11 @@
{#if album.hasSharedLink && isOwned} - @@ -516,22 +517,24 @@ {#if albumHasViewers} - {/if} {#if isOwned} - {/if}
@@ -628,11 +631,14 @@ {#if viewMode === AlbumPageViewMode.VIEW} goto(backUrl)}> {#snippet trailing()} - + {#if isEditor} - { assetStore.suspendTransitions = true; viewMode = AlbumPageViewMode.SELECT_ASSETS; @@ -647,18 +653,46 @@ {/if} {#if isOwned} - + {/if} - + {#if $featureFlags.loaded && $featureFlags.map} + + {/if} {#if album.assetCount > 0} - - + + {/if} {#if isOwned} - + {#if album.assetCount > 0} import { afterNavigate, goto, invalidateAll } from '$app/navigation'; import { page } from '$app/stores'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; @@ -30,6 +29,7 @@ import { cancelMultiselect } from '$lib/utils/asset-utils'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; + import { IconButton } from '@immich/ui'; import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -135,7 +135,14 @@ clearSelect={() => cancelMultiselect(assetInteraction)} > - + cancelMultiselect(assetInteraction)} /> cancelMultiselect(assetInteraction)} shared /> diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 32600cdec3a..e1aa6f9d27e 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -3,7 +3,6 @@ import { page } from '$app/state'; import { shortcut } from '$lib/actions/shortcut'; import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; @@ -45,6 +44,7 @@ searchSmart, type SmartSearchDto, } from '@immich/sdk'; + import { IconButton } from '@immich/ui'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { tick } from 'svelte'; import { t } from 'svelte-i18n'; @@ -269,7 +269,14 @@ clearSelect={() => cancelMultiselect(assetInteraction)} > - + @@ -401,7 +408,14 @@ clearSelect={() => cancelMultiselect(assetInteraction)} > - + diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte index c434bc7de6c..fb3af288236 100644 --- a/web/src/routes/(user)/user-settings/+page.svelte +++ b/web/src/routes/(user)/user-settings/+page.svelte @@ -1,10 +1,9 @@
@@ -61,11 +140,20 @@
- + + +
diff --git a/web/src/routes/auth/onboarding/+page.ts b/web/src/routes/auth/onboarding/+page.ts index 86c19c10a80..66cb3de2c10 100644 --- a/web/src/routes/auth/onboarding/+page.ts +++ b/web/src/routes/auth/onboarding/+page.ts @@ -3,7 +3,7 @@ import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { - await authenticate(url, { admin: true }); + await authenticate(url); const $t = await getFormatter(); diff --git a/web/src/test-data/factories/preferences-factory.ts b/web/src/test-data/factories/preferences-factory.ts new file mode 100644 index 00000000000..d531bc1a99f --- /dev/null +++ b/web/src/test-data/factories/preferences-factory.ts @@ -0,0 +1,43 @@ +import type { UserPreferencesResponseDto } from '@immich/sdk'; +import { Sync } from 'factory.ts'; + +export const preferencesFactory = Sync.makeFactory({ + cast: { + gCastEnabled: false, + }, + download: { + archiveSize: 0, + includeEmbeddedVideos: false, + }, + emailNotifications: { + albumInvite: false, + albumUpdate: false, + enabled: false, + }, + folders: { + enabled: false, + sidebarWeb: false, + }, + memories: { + enabled: false, + }, + people: { + enabled: false, + sidebarWeb: false, + }, + purchase: { + hideBuyButtonUntil: '', + showSupportBadge: false, + }, + ratings: { + enabled: false, + }, + sharedLinks: { + enabled: false, + sidebarWeb: false, + }, + tags: { + enabled: false, + sidebarWeb: false, + }, +});
{exclusionPattern} - { - editExclusionPattern = listIndex; - editedExclusionPattern = exclusionPattern; - }} + onclick={() => onEditExclusionPattern(listIndex)} + aria-label={$t('edit_exclusion_pattern')} + size="small" />
- +