diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml index db20390255..c5db2a6b32 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -15,7 +15,7 @@ services: volumes: - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - /etc/localtime:/etc/localtime:ro - - pnpm_store_server:/buildcache/pnpm-store + - build_cache:/buildcache - ../packages/plugin-core:/build/plugins/immich-plugin-core immich-web: env_file: !reset [] diff --git a/.devcontainer/server/container-start-frontend.sh b/.devcontainer/server/container-start-frontend.sh index 9a0d617d41..e7f1dcc1d0 100755 --- a/.devcontainer/server/container-start-frontend.sh +++ b/.devcontainer/server/container-start-frontend.sh @@ -8,6 +8,8 @@ log "Preparing Immich Web Frontend" log "" run_cmd pnpm --filter @immich/sdk install run_cmd pnpm --filter @immich/sdk build +run_cmd pnpm --filter @immich/plugin-sdk install +run_cmd pnpm --filter @immich/plugin-sdk build run_cmd pnpm --filter immich-web install log "Starting Immich Web Frontend" diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 42adf5c72a..df70e8d151 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -49,7 +49,9 @@ jobs: - name: Publish if: ${{ github.event_name == 'release' }} - run: mise run ci-publish + env: + NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }} + run: mise run ci-publish -- --tag "$NPM_TAG" docker: name: Docker @@ -102,7 +104,7 @@ jobs: name=ghcr.io/${{ github.repository_owner }}/immich-cli tags: | type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }} - type=raw,value=latest,enable=${{ github.event_name == 'release' }} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} - name: Build and push image uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8e16894b49..a76d6fbaa2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -132,7 +132,7 @@ jobs: suffixes: '-rocm' platforms: linux/amd64 runner-mapping: '{"linux/amd64": "pokedex-large"}' - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0 permissions: contents: read actions: read @@ -147,7 +147,7 @@ jobs: platforms: ${{ matrix.platforms }} runner-mapping: ${{ matrix.runner-mapping }} suffixes: ${{ matrix.suffixes }} - dockerhub-push: ${{ github.event_name == 'release' }} + dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} build-args: | DEVICE=${{ matrix.device }} @@ -155,7 +155,7 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0 permissions: contents: read actions: read @@ -167,7 +167,7 @@ jobs: image: immich-server context: . dockerfile: server/Dockerfile - dockerhub-push: ${{ github.event_name == 'release' }} + dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} build-args: | DEVICE=cpu diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 3b789e810d..5438242b50 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -98,9 +98,16 @@ jobs: shouldDeploy: true }; } else if (eventType == "release") { + const tag = context.payload.workflow_run.head_branch; + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag, + }); parameters = { event: "release", - name: context.payload.workflow_run.head_branch, + name: tag, + prerelease: release.prerelease, shouldDeploy: !isFork }; } @@ -146,6 +153,7 @@ jobs: const parameters = JSON.parse(process.env.PARAM_JSON); core.setOutput("event", parameters.event); core.setOutput("name", parameters.name); + core.setOutput("prerelease", parameters.prerelease); core.setOutput("shouldDeploy", parameters.shouldDeploy); - name: Download artifact @@ -203,7 +211,7 @@ jobs: run: mise run //docs:deploy - name: Deploy Docs Release Domain - if: ${{ steps.parameters.outputs.event == 'release' }} + if: ${{ steps.parameters.outputs.event == 'release' && steps.parameters.outputs.prerelease != 'true' }} env: TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 4f6a7dc75b..2cb15eaf62 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -137,7 +137,7 @@ jobs: github-token: ${{ steps.generate-token.outputs.token }} - name: Create draft release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: draft: true tag_name: ${{ needs.bump_version.outputs.version }} diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index ce632fafb7..bb3b65646c 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -39,4 +39,6 @@ jobs: run: pnpm --filter @immich/sdk build - name: Publish - run: pnpm --filter @immich/sdk publish --provenance --no-git-checks + env: + NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }} + run: pnpm --filter @immich/sdk publish --provenance --no-git-checks --tag "$NPM_TAG" diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index d347317793..6fdd7af18b 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -72,10 +72,6 @@ jobs: run: flutter pub get working-directory: ./mobile/packages/ui - - name: Install dependencies for UI Showcase - run: flutter pub get - working-directory: ./mobile/packages/ui/showcase - - name: Generate translation files run: mise //mobile:codegen:translation diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e16e6f059d..918e0771c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -374,7 +374,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -451,7 +451,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/Makefile b/Makefile index 648aed5120..eb8bad09f3 100644 --- a/Makefile +++ b/Makefile @@ -1,46 +1,46 @@ dev: - @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans + @printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1 dev-down: - docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans + @printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1 dev-update: - @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans + @printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1 dev-scale: - @trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans + @printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1 dev-docs: npm --prefix docs run start .PHONY: e2e e2e: - @trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans + @printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1 e2e-dev: - @trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.dev.yml up --remove-orphans + @printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1 e2e-update: - @trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans + @printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1 e2e-down: - docker compose -f ./e2e/docker-compose.yml down --remove-orphans + @printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1 prod: - @trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans + @printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1 prod-down: - docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans + @printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1 prod-scale: - @trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans + @printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1 .PHONY: open-api open-api: - @printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1 + @printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1 sql: - @printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1 + @printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1 renovate: @@ -52,16 +52,7 @@ renovate: MODULES = e2e server web cli sdk docs .github test-e2e: - docker compose -f ./e2e/docker-compose.yml build - pnpm --filter immich-e2e run test - pnpm --filter immich-e2e run test:web + @printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1 clean: - find . -name "node_modules" -type d -prune -exec rm -rf {} + - find . -name "dist" -type d -prune -exec rm -rf '{}' + - find . -name "build" -type d -prune -exec rm -rf '{}' + - find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' + - find . -name "coverage" -type d -prune -exec rm -rf '{}' + - find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' + - command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true - command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true + @printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1 diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index dd72baa45b..cac9f501d8 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -109,6 +109,24 @@ mise //mobile:translation The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above. +#### UI components and widget previews + +Shared design-system widgets (buttons, inputs, forms) live in the +[`immich_ui` package](https://github.com/immich-app/immich/tree/main/mobile/packages/ui/) +under `mobile/packages/ui/`. Components are defined in `lib/src/components/` +and have matching previews in `lib/src/previews/`. + +To inspect a component in isolation with a light/dark toggle and hot reload, +launch [Flutter's Widget Previewer](https://docs.flutter.dev/tools/widget-previewer): + +```bash +cd mobile/packages/ui +flutter widget-preview start +``` + +In VS Code or Android Studio with the Flutter plugin, the previewer +auto-starts when you open the **Flutter Widget Preview** tab in the sidebar. + ## IDE setup ### Lint / format extensions diff --git a/e2e/mise.toml b/e2e/mise.toml index 99056f9ead..b149922564 100644 --- a/e2e/mise.toml +++ b/e2e/mise.toml @@ -1,11 +1,21 @@ [tasks.install] run = "pnpm install --filter immich-e2e --frozen-lockfile" +[tasks.build] +dir = "{{ config_root }}" +run = "docker compose build" + [tasks.test] +depends = ["//e2e:build", "//e2e:ci-setup"] env._.path = "./node_modules/.bin" run = "vitest --run" +[tasks.playwright-install] +env._.path = "./node_modules/.bin" +run = "playwright install" + [tasks."test-web"] +depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"] env._.path = "./node_modules/.bin" run = "playwright test" @@ -30,7 +40,12 @@ run = "tsc --noEmit" [tasks.ci-setup] -depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"] +depends = [ + "//:sdk:install", + "//:sdk:build", + "//packages/cli:install", + "//packages/cli:build", +] run = { task = ":install" } diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index f2558a32ad..902c5302e5 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -95,6 +95,7 @@ describe('/server', () => { major: expect.any(Number), minor: expect.any(Number), patch: expect.any(Number), + prerelease: null, }); }); }); @@ -115,6 +116,7 @@ describe('/server', () => { oauthAutoLaunch: false, ocr: false, passwordLogin: true, + realtimeTranscoding: false, search: true, sidecar: true, trash: true, diff --git a/e2e/src/specs/server/api/system-config.e2e-spec.ts b/e2e/src/specs/server/api/system-config.e2e-spec.ts index 1bd7bdc489..91b747cf28 100644 --- a/e2e/src/specs/server/api/system-config.e2e-spec.ts +++ b/e2e/src/specs/server/api/system-config.e2e-spec.ts @@ -21,18 +21,18 @@ describe('/system-config', () => { const response1 = await request(app) .put('/system-config') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ ...config, newVersionCheck: { enabled: false } }); + .send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } }); expect(response1.status).toBe(200); - expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } }); + expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } }); const response2 = await request(app) .put('/system-config') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ ...config, newVersionCheck: { enabled: true } }); + .send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } }); expect(response2.status).toBe(200); - expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } }); + expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } }); }); it('should reject an invalid config entry', async () => { diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index 52dfa4c493..553ce18005 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -55,8 +55,8 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe result.duration.push(asset.duration); result.projectionType.push(asset.projectionType); result.livePhotoVideoId.push(asset.livePhotoVideoId); - result.city.push(asset.city); - result.country.push(asset.country); + result.city?.push(asset.city); + result.country?.push(asset.country); result.visibility.push(asset.visibility); } diff --git a/i18n/en.json b/i18n/en.json index caebf53452..f4ad3001c2 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -305,6 +305,8 @@ "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.", + "release_channel_release_candidate": "Release candidate", + "release_channel_stable": "Stable", "remove_failed_jobs": "Remove failed jobs", "require_password_change_on_login": "Require user to change password on first login", "reset_settings_to_default": "Reset settings to default", @@ -399,6 +401,10 @@ "transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.", "transcoding_preset_preset": "Preset (-preset)", "transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.", + "transcoding_realtime": "Real-time Transcoding [EXPERIMENTAL]", + "transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.", + "transcoding_realtime_enabled": "Enable real-time transcoding", + "transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.", "transcoding_reference_frames": "Reference frames", "transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.", "transcoding_required_description": "Only videos not in an accepted format", @@ -442,6 +448,8 @@ "user_settings_description": "Manage user settings", "user_successfully_removed": "User {email} has been successfully removed.", "users_page_description": "Admin users page", + "version_check_channel": "Release channel", + "version_check_channel_description": "Pick the release channel you want to get version announcements for", "version_check_enabled_description": "Enable version check", "version_check_implications": "The version check feature relies on periodic communication with {server}", "version_check_settings": "Version Check", @@ -2235,6 +2243,7 @@ "slideshow_repeat": "Repeat slideshow", "slideshow_repeat_description": "Loop back to beginning when slideshow ends", "slideshow_settings": "Slideshow settings", + "smart_album": "Smart album", "sort_albums_by": "Sort albums by...", "sort_created": "Date created", "sort_items": "Number of items", @@ -2453,6 +2462,7 @@ "video": "Video", "video_hover_setting": "Play video thumbnail on hover", "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.", + "video_quality": "Video quality", "videos": "Videos", "videos_count": "{count, plural, one {# Video} other {# Videos}}", "videos_only": "Videos only", diff --git a/machine-learning/immich_ml/models/ocr/recognition.py b/machine-learning/immich_ml/models/ocr/recognition.py index 6408e4818f..94f40c9285 100644 --- a/machine-learning/immich_ml/models/ocr/recognition.py +++ b/machine-learning/immich_ml/models/ocr/recognition.py @@ -64,6 +64,7 @@ class TextRecognizer(InferenceModel): rec_batch_num=max_batch_size if max_batch_size else 6, rec_img_shape=(3, 48, 320), lang_type=self.language, + model_root_dir=self.cache_dir, ) ) return session diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py index b281c0d417..5145be0045 100644 --- a/machine-learning/test_main.py +++ b/machine-learning/test_main.py @@ -1028,7 +1028,12 @@ class TestOcr: text_recognizer.load() rapid_recognizer.assert_called_once_with( - OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320)) + OcrOptions( + session=ort_session.return_value, + rec_batch_num=6, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, + ) ) def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None: @@ -1041,7 +1046,12 @@ class TestOcr: text_recognizer.load() rapid_recognizer.assert_called_once_with( - OcrOptions(session=ort_session.return_value, rec_batch_num=4, rec_img_shape=(3, 48, 320)) + OcrOptions( + session=ort_session.return_value, + rec_batch_num=4, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, + ) ) def test_ignore_other_custom_max_batch_size( @@ -1056,7 +1066,12 @@ class TestOcr: text_recognizer.load() rapid_recognizer.assert_called_once_with( - OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320)) + OcrOptions( + session=ort_session.return_value, + rec_batch_num=6, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, + ) ) diff --git a/mise.toml b/mise.toml index 0e4d27cc51..8cff3993f7 100644 --- a/mise.toml +++ b/mise.toml @@ -54,8 +54,8 @@ lockfile = true [tasks.plugins] run = [ - "pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile", - "pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build", + "pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile", + "pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core build", ] [tasks.open-api-typescript] @@ -84,6 +84,72 @@ run = [ dir = "server" run = "node ./dist/bin/sync-sql.js" +# TODO dev, prod, and e2e should be de-duplicated by using env but for some reason I ran into issues +[tasks.dev] +depends = "//:plugins" +dir = "docker" +interactive = true +env = { COMPOSE_BAKE = true } +run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans" +depends_post = "//:dev-down" + +[tasks.dev-update] +run = { task = "//:dev", args = ["--build", "-V"] } + +[tasks.dev-scale] +run = { task = "//:dev", args = ["--build", "-V", "--scale immich-server=3"] } + +[tasks.dev-down] +dir = "docker" +run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans" + +[tasks.prod] +depends = "//:plugins" +dir = "docker" +interactive = true +env = { COMPOSE_BAKE = true } +run = "docker compose -f ./docker-compose.prod.yml up --build --remove-orphans" +depends_post = "//:prod-down" + +[tasks.prod-scale] +run = { task = "//:prod", args = [ + "--build", + "-V", + "--scale immich-server=3", + "--scale immich-microservices", +] } + +[tasks.prod-down] +dir = "docker" +run = "docker compose -f ./docker-compose.prod.yml down --remove-orphans" + +[tasks.e2e] +depends = "//:plugins" +dir = "e2e" +interactive = true +env = { COMPOSE_BAKE = true } +run = "docker compose -f ./docker-compose.yml up --remove-orphans" +depends_post = "//:e2e-down" + +[tasks.e2e-dev] +depends = "//:plugins" +dir = "e2e" +interactive = true +env = { COMPOSE_BAKE = true } +run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans" +depends_post = "//:e2e-dev-down" + +[tasks.e2e-update] +run = { task = "//:e2e", args = ["--build", '-V'] } + +[tasks.e2e-down] +dir = "e2e" +run = "docker compose -f ./docker-compose.yml down --remove-orphans" + +[tasks.e2e-dev-down] +dir = "e2e" +run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans" + # SDK tasks [tasks."sdk:install"] dir = "packages/sdk" @@ -99,3 +165,14 @@ run = "pnpm format" [tasks."i18n:format-fix"] run = "pnpm format:fix" + +[tasks.clean] +run = [ + "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +", + "find . -name 'dist' -type d -prune -exec rm -rf '{}' +", + "find . -name 'build' -type d -prune -exec rm -rf '{}' +", + "find . -name '.svelte-kit' -type d -prune -exec rm -rf '{}' +", + "find . -name 'coverage' -type d -prune -exec rm -rf '{}' +", + "find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +", + { task = "//:*-down" }, +] diff --git a/mobile/drift_schemas/main/drift_schema_v27.json b/mobile/drift_schemas/main/drift_schema_v27.json new file mode 100644 index 0000000000..4df6ef8389 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v27.json @@ -0,0 +1,3368 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.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": "email", + "getter_name": "email", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "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": "width", + "getter_name": "width", + "moor_type": "int", + "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": "duration_ms", + "getter_name": "durationMs", + "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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": "uploaded_at", + "getter_name": "uploadedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "live_photo_video_id", + "getter_name": "livePhotoVideoId", + "moor_type": "string", + "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" + } + }, + { + "name": "stack_id", + "getter_name": "stackId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "library_id", + "getter_name": "libraryId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_edited", + "getter_name": "isEdited", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_edited\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_edited\" 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": 2, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "stack_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": "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": "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "primary_asset_id", + "getter_name": "primaryAssetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "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": "width", + "getter_name": "width", + "moor_type": "int", + "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": "duration_ms", + "getter_name": "durationMs", + "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": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "i_cloud_id", + "getter_name": "iCloudId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_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": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'\\'')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "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": "thumbnail_asset_id", + "getter_name": "thumbnailAssetId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "is_activity_enabled", + "getter_name": "isActivityEnabled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_activity_enabled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_activity_enabled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "order", + "getter_name": "order", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumAssetOrder.values)", + "dart_type_name": "AlbumAssetOrder" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [ + 4 + ], + "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": "is_ios_shared_album", + "getter_name": "isIosSharedAlbum", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_ios_shared_album\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_ios_shared_album\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "linked_remote_album_id", + "getter_name": "linkedRemoteAlbumId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "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": 6, + "references": [ + 3, + 5 + ], + "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": [ + { + "foreign_key": { + "to": { + "table": "local_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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": [ + { + "foreign_key": { + "to": { + "table": "local_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 7, + "references": [ + 6 + ], + "type": "index", + "data": { + "on": 6, + "name": "idx_local_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 8, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 9, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 10, + "references": [ + 2 + ], + "type": "index", + "data": { + "on": 2, + "name": "idx_stack_primary_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 11, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 12, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_library_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 13, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 14, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_stack_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 15, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n", + "unique": false, + "columns": [] + } + }, + { + "id": 16, + "references": [], + "type": "table", + "data": { + "name": "auth_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": "email", + "getter_name": "email", + "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": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + }, + { + "name": "quota_size_in_bytes", + "getter_name": "quotaSizeInBytes", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "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": [] + }, + { + "name": "pin_code", + "getter_name": "pinCode", + "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": [ + "id" + ] + } + }, + { + "id": 17, + "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "key", + "getter_name": "key", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(UserMetadataKey.values)", + "dart_type_name": "UserMetadataKey" + } + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "userMetadataConverter", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "user_id", + "key" + ] + } + }, + { + "id": 18, + "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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": 19, + "references": [ + 1 + ], + "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": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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": "double", + "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": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "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": "lens", + "getter_name": "lens", + "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": 20, + "references": [ + 1, + 4 + ], + "type": "table", + "data": { + "name": "remote_album_asset_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": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 21, + "references": [ + 4, + 0 + ], + "type": "table", + "data": { + "name": "remote_album_user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "role", + "getter_name": "role", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumUserRole.values)", + "dart_type_name": "AlbumUserRole" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "album_id", + "user_id" + ] + } + }, + { + "id": 22, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_asset_cloud_id_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": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "cloud_id", + "getter_name": "cloudId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "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": 23, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "memory_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": "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": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "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(MemoryTypeEnum.values)", + "dart_type_name": "MemoryTypeEnum" + } + }, + { + "name": "data", + "getter_name": "data", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_saved", + "getter_name": "isSaved", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_saved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_saved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "memory_at", + "getter_name": "memoryAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "seen_at", + "getter_name": "seenAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "show_at", + "getter_name": "showAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "hide_at", + "getter_name": "hideAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 24, + "references": [ + 1, + 23 + ], + "type": "table", + "data": { + "name": "memory_asset_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": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "memory_id", + "getter_name": "memoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES memory_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES memory_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "memory_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "memory_id" + ] + } + }, + { + "id": 25, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "person_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": "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": "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": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "face_asset_id", + "getter_name": "faceAssetId", + "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": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_hidden", + "getter_name": "isHidden", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_hidden\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "color", + "getter_name": "color", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "birth_date", + "getter_name": "birthDate", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 26, + "references": [ + 1, + 25 + ], + "type": "table", + "data": { + "name": "asset_face_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": "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": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "person_id", + "getter_name": "personId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES person_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES person_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "person_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "image_width", + "getter_name": "imageWidth", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "image_height", + "getter_name": "imageHeight", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x1", + "getter_name": "boundingBoxX1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y1", + "getter_name": "boundingBoxY1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x2", + "getter_name": "boundingBoxX2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y2", + "getter_name": "boundingBoxY2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source_type", + "getter_name": "sourceType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_visible\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "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": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 27, + "references": [], + "type": "table", + "data": { + "name": "store_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "string_value", + "getter_name": "stringValue", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "int_value", + "getter_name": "intValue", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 28, + "references": [], + "type": "table", + "data": { + "name": "trashed_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": "width", + "getter_name": "width", + "moor_type": "int", + "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": "duration_ms", + "getter_name": "durationMs", + "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": "album_id", + "getter_name": "albumId", + "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": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source", + "getter_name": "source", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(TrashOrigin.values)", + "dart_type_name": "TrashOrigin" + } + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id", + "album_id" + ] + } + }, + { + "id": 29, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "asset_edit_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": "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": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "action", + "getter_name": "action", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetEditAction.values)", + "dart_type_name": "AssetEditAction" + } + }, + { + "name": "parameters", + "getter_name": "parameters", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "editParameterConverter", + "dart_type_name": "Map" + } + }, + { + "name": "sequence", + "getter_name": "sequence", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 30, + "references": [], + "type": "table", + "data": { + "name": "settings", + "was_declared_in_moor": false, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "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": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "key" + ] + } + }, + { + "id": 31, + "references": [ + 18 + ], + "type": "index", + "data": { + "on": 18, + "name": "idx_partner_shared_with_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 32, + "references": [ + 19 + ], + "type": "index", + "data": { + "on": 19, + "name": "idx_lat_lng", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)", + "unique": false, + "columns": [] + } + }, + { + "id": 33, + "references": [ + 19 + ], + "type": "index", + "data": { + "on": 19, + "name": "idx_remote_exif_city", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 34, + "references": [ + 20 + ], + "type": "index", + "data": { + "on": 20, + "name": "idx_remote_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 35, + "references": [ + 22 + ], + "type": "index", + "data": { + "on": 22, + "name": "idx_remote_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 36, + "references": [ + 25 + ], + "type": "index", + "data": { + "on": 25, + "name": "idx_person_owner_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 37, + "references": [ + 26 + ], + "type": "index", + "data": { + "on": 26, + "name": "idx_asset_face_person_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 38, + "references": [ + 26 + ], + "type": "index", + "data": { + "on": 26, + "name": "idx_asset_face_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 39, + "references": [ + 26 + ], + "type": "index", + "data": { + "on": 26, + "name": "idx_asset_face_visible_person", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 40, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_trashed_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 41, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_trashed_local_asset_album", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 42, + "references": [ + 29 + ], + "type": "index", + "data": { + "on": 29, + "name": "idx_asset_edit_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)", + "unique": false, + "columns": [] + } + } + ], + "fixed_sql": [ + { + "name": "user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NOT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"local_date_time\" TEXT NULL, \"thumb_hash\" TEXT NULL, \"deleted_at\" TEXT NULL, \"uploaded_at\" TEXT NULL, \"live_photo_video_id\" TEXT NULL, \"visibility\" INTEGER NOT NULL, \"stack_id\" TEXT NULL, \"library_id\" TEXT NULL, \"is_edited\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_edited\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "stack_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"stack_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"primary_asset_id\" TEXT NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"description\" TEXT NOT NULL DEFAULT '', \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"thumbnail_asset_id\" TEXT NULL REFERENCES remote_asset_entity (id) ON DELETE SET NULL, \"is_activity_enabled\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_activity_enabled\" IN (0, 1)), \"order\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"backup_selection\" INTEGER NOT NULL, \"is_ios_shared_album\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_ios_shared_album\" IN (0, 1)), \"linked_remote_album_id\" TEXT NULL REFERENCES remote_album_entity (id) ON DELETE SET NULL, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES local_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES local_album_entity (id) ON DELETE CASCADE, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_local_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_local_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)" + } + ] + }, + { + "name": "idx_stack_primary_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_library_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)" + } + ] + }, + { + "name": "idx_remote_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_remote_asset_stack_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)" + } + ] + }, + { + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)" + } + ] + }, + { + "name": "auth_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"auth_user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"is_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_admin\" IN (0, 1)), \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL, \"quota_size_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"quota_usage_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"pin_code\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "user_metadata_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_metadata_entity\" (\"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"key\" INTEGER NOT NULL, \"value\" BLOB NOT NULL, PRIMARY KEY (\"user_id\", \"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "partner_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"partner_entity\" (\"shared_by_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"shared_with_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"in_timeline\" INTEGER NOT NULL DEFAULT 0 CHECK (\"in_timeline\" IN (0, 1)), PRIMARY KEY (\"shared_by_id\", \"shared_with_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_exif_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_exif_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"city\" TEXT NULL, \"state\" TEXT NULL, \"country\" TEXT NULL, \"date_time_original\" TEXT NULL, \"description\" TEXT NULL, \"height\" INTEGER NULL, \"width\" INTEGER NULL, \"exposure_time\" TEXT NULL, \"f_number\" REAL NULL, \"file_size\" INTEGER NULL, \"focal_length\" REAL NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"iso\" INTEGER NULL, \"make\" TEXT NULL, \"model\" TEXT NULL, \"lens\" TEXT NULL, \"orientation\" TEXT NULL, \"time_zone\" TEXT NULL, \"rating\" INTEGER NULL, \"projection_type\" TEXT NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_user_entity\" (\"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, \"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"role\" INTEGER NOT NULL, PRIMARY KEY (\"album_id\", \"user_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_cloud_id_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_cloud_id_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"cloud_id\" TEXT NULL, \"created_at\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"deleted_at\" TEXT NULL, \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"type\" INTEGER NOT NULL, \"data\" TEXT NOT NULL, \"is_saved\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_saved\" IN (0, 1)), \"memory_at\" TEXT NOT NULL, \"seen_at\" TEXT NULL, \"show_at\" TEXT NULL, \"hide_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"memory_id\" TEXT NOT NULL REFERENCES memory_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"memory_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "person_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"person_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"name\" TEXT NOT NULL, \"face_asset_id\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL CHECK (\"is_favorite\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL CHECK (\"is_hidden\" IN (0, 1)), \"color\" TEXT NULL, \"birth_date\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_face_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_face_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"person_id\" TEXT NULL REFERENCES person_entity (id) ON DELETE SET NULL, \"image_width\" INTEGER NOT NULL, \"image_height\" INTEGER NOT NULL, \"bounding_box_x1\" INTEGER NOT NULL, \"bounding_box_y1\" INTEGER NOT NULL, \"bounding_box_x2\" INTEGER NOT NULL, \"bounding_box_y2\" INTEGER NOT NULL, \"source_type\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), \"deleted_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "store_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"store_entity\" (\"id\" INTEGER NOT NULL, \"string_value\" TEXT NULL, \"int_value\" INTEGER NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "trashed_local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"album_id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"source\" INTEGER NOT NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_edit_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_edit_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"action\" INTEGER NOT NULL, \"parameters\" BLOB NOT NULL, \"sequence\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "settings", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"settings\" (\"key\" TEXT NOT NULL, \"value\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_partner_shared_with_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)" + } + ] + }, + { + "name": "idx_lat_lng", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)" + } + ] + }, + { + "name": "idx_remote_exif_city", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL" + } + ] + }, + { + "name": "idx_remote_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_remote_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)" + } + ] + }, + { + "name": "idx_person_owner_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)" + } + ] + }, + { + "name": "idx_asset_face_person_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)" + } + ] + }, + { + "name": "idx_asset_face_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)" + } + ] + }, + { + "name": "idx_asset_face_visible_person", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL" + } + ] + }, + { + "name": "idx_trashed_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_trashed_local_asset_album", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)" + } + ] + }, + { + "name": "idx_asset_edit_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)" + } + ] + } + ] +} \ No newline at end of file diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index e3325525eb..42926ef45e 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; + 467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -22,7 +22,7 @@ B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; }; B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; }; B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; }; - D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; }; + D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */; }; F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; F0B57D3A2DF764BD00DC5BCC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */; }; F0B57D3C2DF764BD00DC5BCC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */; }; @@ -85,16 +85,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; + 614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -103,7 +105,6 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = ""; }; - B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = ""; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = ""; }; B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = ""; }; @@ -111,12 +112,11 @@ B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = ""; }; B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = ""; }; B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = ""; }; - E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; + CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; - F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -199,7 +199,7 @@ FEE084F82EC172460045228E /* SQLiteData in Frameworks */, FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */, FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */, - D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */, + D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -216,7 +216,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */, + 467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -226,12 +226,12 @@ 0FB772A5B9601143383626CA /* Pods */ = { isa = PBXGroup; children = ( - 2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */, - E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */, - F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */, - F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */, - 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */, - B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */, + 614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */, + 6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */, + 681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */, + 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */, + 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */, + C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -239,10 +239,10 @@ 1754452DD81DA6620E279E51 /* Frameworks */ = { isa = PBXGroup; children = ( - 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */, - 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */, F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */, F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */, + CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */, + 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -370,7 +370,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */, + BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -378,8 +378,8 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */, - 6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */, + 513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */, + 2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -393,6 +393,9 @@ FEE084F22EC172080045228E /* Schemas */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; productType = "com.apple.product-type.application"; @@ -421,7 +424,7 @@ isa = PBXNativeTarget; buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */; buildPhases = ( - 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */, + 8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */, FAC6F88C2D287C890078CB2F /* Sources */, FAC6F88D2D287C890078CB2F /* Frameworks */, FAC6F88E2D287C890078CB2F /* Resources */, @@ -470,7 +473,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */, FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */, ); @@ -517,6 +520,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -533,7 +553,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */ = { + 513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -555,7 +592,22 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -577,55 +629,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; - }; - D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1092,7 +1095,7 @@ }; FAC6F89C2D287C890078CB2F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */; + baseConfigurationReference = 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1135,7 +1138,7 @@ }; FAC6F89D2D287C890078CB2F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */; + baseConfigurationReference = 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1175,7 +1178,7 @@ }; FAC6F89E2D287C890078CB2F /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */; + baseConfigurationReference = C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1258,6 +1261,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = { isa = XCRemoteSwiftPackageReference; @@ -1278,6 +1288,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; FEE084F72EC172460045228E /* SQLiteData */ = { isa = XCSwiftPackageProductDependency; package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */; diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index ed1e2d084d..0e2d8e8a3d 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "5928286acce13def418ec36d05a001a9641086f2", - "version" : "1.0.3" + "revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6", + "version" : "1.1.0" } }, { diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index f5f7f7887a..17fa91786e 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -49,7 +49,7 @@ def get_version_from_pubspec pubspec = YAML.load_file(pubspec_path) version_string = pubspec['version'] - version_string ? version_string.split('+').first : nil + version_string ? version_string.split('+').first.split('-').first : nil end # Helper method to configure code signing for all targets diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 409cd38431..b7fec86b55 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -23,6 +23,7 @@ const String kBackupLivePhotoGroup = 'backup_live_photo_group'; const String kDownloadGroupImage = 'group_image'; const String kDownloadGroupVideo = 'group_video'; const String kDownloadGroupLivePhoto = 'group_livephoto'; +const String kShareDownloadGroup = 'group_share'; // Timeline constants const int kTimelineNoneSegmentSize = 120; diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index 1baa368df4..a955fb73a8 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -1,14 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/config/album_config.dart'; import 'package:immich_mobile/domain/models/config/backup_config.dart'; import 'package:immich_mobile/domain/models/config/cleanup_config.dart'; import 'package:immich_mobile/domain/models/config/image_config.dart'; import 'package:immich_mobile/domain/models/config/map_config.dart'; +import 'package:immich_mobile/domain/models/config/network_config.dart'; import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; import 'package:immich_mobile/domain/models/config/theme_config.dart'; import 'package:immich_mobile/domain/models/config/timeline_config.dart'; import 'package:immich_mobile/domain/models/config/viewer_config.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; + +const defaultConfig = AppConfig(); class AppConfig { + final LogLevel logLevel; final ThemeConfig theme; final CleanupConfig cleanup; final MapConfig map; @@ -18,8 +29,10 @@ class AppConfig { final SlideshowConfig slideshow; final AlbumConfig album; final BackupConfig backup; + final NetworkConfig network; const AppConfig({ + this.logLevel = .info, this.theme = const .new(), this.cleanup = const .new(), this.map = const .new(), @@ -29,9 +42,11 @@ class AppConfig { this.slideshow = const .new(), this.album = const .new(), this.backup = const .new(), + this.network = const .new(), }); AppConfig copyWith({ + LogLevel? logLevel, ThemeConfig? theme, CleanupConfig? cleanup, MapConfig? map, @@ -41,7 +56,9 @@ class AppConfig { SlideshowConfig? slideshow, AlbumConfig? album, BackupConfig? backup, + NetworkConfig? network, }) => .new( + logLevel: logLevel ?? this.logLevel, theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, map: map ?? this.map, @@ -51,12 +68,14 @@ class AppConfig { slideshow: slideshow ?? this.slideshow, album: album ?? this.album, backup: backup ?? this.backup, + network: network ?? this.network, ); @override bool operator ==(Object other) => identical(this, other) || (other is AppConfig && + other.logLevel == logLevel && other.theme == theme && other.cleanup == cleanup && other.map == map && @@ -65,12 +84,113 @@ class AppConfig { other.viewer == viewer && other.slideshow == slideshow && other.album == album && - other.backup == backup); + other.backup == backup && + other.network == network); @override - int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album, backup); + int get hashCode => + Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network); @override String toString() => - 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup)'; + 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)'; + + T read(SettingsKey key) => + (switch (key) { + .logLevel => logLevel, + .themePrimaryColor => theme.primaryColor, + .themeMode => theme.mode, + .themeDynamic => theme.dynamicTheme, + .themeColorfulInterface => theme.colorfulInterface, + .imagePreferRemote => image.preferRemote, + .imageLoadOriginal => image.loadOriginal, + .viewerLoopVideo => viewer.loopVideo, + .viewerLoadOriginalVideo => viewer.loadOriginalVideo, + .viewerAutoPlayVideo => viewer.autoPlayVideo, + .viewerTapToNavigate => viewer.tapToNavigate, + .networkAutoEndpointSwitching => network.autoEndpointSwitching, + .networkPreferredWifiName => network.preferredWifiName, + .networkLocalEndpoint => network.localEndpoint, + .networkExternalEndpointList => network.externalEndpointList, + .networkCustomHeaders => network.customHeaders, + .albumSortMode => album.sortMode, + .albumIsReverse => album.isReverse, + .albumIsGrid => album.isGrid, + .backupEnabled => backup.enabled, + .backupUseCellularForVideos => backup.useCellularForVideos, + .backupUseCellularForPhotos => backup.useCellularForPhotos, + .backupRequireCharging => backup.requireCharging, + .backupTriggerDelay => backup.triggerDelay, + .backupSyncAlbums => backup.syncAlbums, + .timelineTilesPerRow => timeline.tilesPerRow, + .timelineGroupAssetsBy => timeline.groupAssetsBy, + .timelineStorageIndicator => timeline.storageIndicator, + .mapShowFavoriteOnly => map.favoritesOnly, + .mapRelativeDate => map.relativeDays, + .mapIncludeArchived => map.includeArchived, + .mapThemeMode => map.themeMode, + .mapWithPartners => map.withPartners, + .cleanupKeepFavorites => cleanup.keepFavorites, + .cleanupKeepMediaType => cleanup.keepMediaType, + .cleanupKeepAlbumIds => cleanup.keepAlbumIds, + .cleanupCutoffDaysAgo => cleanup.cutoffDaysAgo, + .cleanupDefaultsInitialized => cleanup.defaultsInitialized, + .slideshowTransition => slideshow.transition, + .slideshowRepeat => slideshow.repeat, + .slideshowDuration => slideshow.duration, + .slideshowLook => slideshow.look, + .slideshowDirection => slideshow.direction, + }) + as T; + + factory AppConfig.fromEntries(Map, Object> overrides) => + overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value)); + + AppConfig write(SettingsKey key, T value) { + return switch (key) { + .logLevel => copyWith(logLevel: value as LogLevel), + .themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)), + .themeMode => copyWith(theme: theme.copyWith(mode: value as ThemeMode)), + .themeDynamic => copyWith(theme: theme.copyWith(dynamicTheme: value as bool)), + .themeColorfulInterface => copyWith(theme: theme.copyWith(colorfulInterface: value as bool)), + .imagePreferRemote => copyWith(image: image.copyWith(preferRemote: value as bool)), + .imageLoadOriginal => copyWith(image: image.copyWith(loadOriginal: value as bool)), + .viewerLoopVideo => copyWith(viewer: viewer.copyWith(loopVideo: value as bool)), + .viewerLoadOriginalVideo => copyWith(viewer: viewer.copyWith(loadOriginalVideo: value as bool)), + .viewerAutoPlayVideo => copyWith(viewer: viewer.copyWith(autoPlayVideo: value as bool)), + .viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)), + .networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)), + .networkPreferredWifiName => copyWith(network: network.copyWith(preferredWifiName: (value as String))), + .networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: (value as String))), + .networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List)), + .networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map)), + .albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)), + .albumIsReverse => copyWith(album: album.copyWith(isReverse: value as bool)), + .albumIsGrid => copyWith(album: album.copyWith(isGrid: value as bool)), + .backupEnabled => copyWith(backup: backup.copyWith(enabled: value as bool)), + .backupUseCellularForVideos => copyWith(backup: backup.copyWith(useCellularForVideos: value as bool)), + .backupUseCellularForPhotos => copyWith(backup: backup.copyWith(useCellularForPhotos: value as bool)), + .backupRequireCharging => copyWith(backup: backup.copyWith(requireCharging: value as bool)), + .backupTriggerDelay => copyWith(backup: backup.copyWith(triggerDelay: value as int)), + .backupSyncAlbums => copyWith(backup: backup.copyWith(syncAlbums: value as bool)), + .timelineTilesPerRow => copyWith(timeline: timeline.copyWith(tilesPerRow: value as int)), + .timelineGroupAssetsBy => copyWith(timeline: timeline.copyWith(groupAssetsBy: value as GroupAssetsBy)), + .timelineStorageIndicator => copyWith(timeline: timeline.copyWith(storageIndicator: value as bool)), + .mapShowFavoriteOnly => copyWith(map: map.copyWith(favoritesOnly: value as bool)), + .mapRelativeDate => copyWith(map: map.copyWith(relativeDays: value as int)), + .mapIncludeArchived => copyWith(map: map.copyWith(includeArchived: value as bool)), + .mapThemeMode => copyWith(map: map.copyWith(themeMode: value as ThemeMode)), + .mapWithPartners => copyWith(map: map.copyWith(withPartners: value as bool)), + .cleanupKeepFavorites => copyWith(cleanup: cleanup.copyWith(keepFavorites: value as bool)), + .cleanupKeepMediaType => copyWith(cleanup: cleanup.copyWith(keepMediaType: value as AssetKeepType)), + .cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List)), + .cleanupCutoffDaysAgo => copyWith(cleanup: cleanup.copyWith(cutoffDaysAgo: value as int)), + .cleanupDefaultsInitialized => copyWith(cleanup: cleanup.copyWith(defaultsInitialized: value as bool)), + .slideshowTransition => copyWith(slideshow: slideshow.copyWith(transition: value as bool)), + .slideshowRepeat => copyWith(slideshow: slideshow.copyWith(repeat: value as bool)), + .slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)), + .slideshowLook => copyWith(slideshow: slideshow.copyWith(look: value as SlideshowLook)), + .slideshowDirection => copyWith(slideshow: slideshow.copyWith(direction: value as SlideshowDirection)), + }; + } } diff --git a/mobile/lib/domain/models/config/network_config.dart b/mobile/lib/domain/models/config/network_config.dart index 78f0482a1a..34666d60e1 100644 --- a/mobile/lib/domain/models/config/network_config.dart +++ b/mobile/lib/domain/models/config/network_config.dart @@ -2,15 +2,15 @@ import 'package:flutter/foundation.dart'; class NetworkConfig { final bool autoEndpointSwitching; - final String? preferredWifiName; - final String? localEndpoint; + final String preferredWifiName; + final String localEndpoint; final List externalEndpointList; final Map customHeaders; const NetworkConfig({ this.autoEndpointSwitching = false, - this.preferredWifiName, - this.localEndpoint, + this.preferredWifiName = '', + this.localEndpoint = '', this.externalEndpointList = const [], this.customHeaders = const {}, }); diff --git a/mobile/lib/domain/models/config/system_config.dart b/mobile/lib/domain/models/config/system_config.dart deleted file mode 100644 index 7d8fef6dd8..0000000000 --- a/mobile/lib/domain/models/config/system_config.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/domain/models/config/network_config.dart'; -import 'package:immich_mobile/domain/models/log.model.dart'; - -class SystemConfig { - final LogLevel logLevel; - final NetworkConfig network; - - const SystemConfig({this.logLevel = .info, this.network = const .new()}); - - SystemConfig copyWith({LogLevel? logLevel, NetworkConfig? network}) => - SystemConfig(logLevel: logLevel ?? this.logLevel, network: network ?? this.network); - - @override - bool operator ==(Object other) => - identical(this, other) || (other is SystemConfig && other.logLevel == logLevel && other.network == network); - - @override - int get hashCode => Object.hash(logLevel, network); - - @override - String toString() => 'SystemConfig(logLevel: $logLevel, network: $network)'; -} diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart deleted file mode 100644 index 541c538169..0000000000 --- a/mobile/lib/domain/models/metadata_key.dart +++ /dev/null @@ -1,273 +0,0 @@ -import 'dart:convert'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/domain/models/config/system_config.dart'; -import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/timeline.model.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; - -enum MetadataDomain { - appConfig('config.app'), - systemConfig('config.system'); - - final String prefix; - const MetadataDomain(this.prefix); -} - -enum MetadataKey { - // Theme - themePrimaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)), - themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)), - themeDynamic(.appConfig, 'theme.dynamic', false), - themeColorfulInterface(.appConfig, 'theme.colorfulInterface', true), - - // Image - imagePreferRemote(.appConfig, 'image.preferRemote', false), - imageLoadOriginal(.appConfig, 'image.loadOriginal', false), - - // Viewer - viewerLoopVideo(.appConfig, 'viewer.loopVideo', true), - viewerLoadOriginalVideo(.appConfig, 'viewer.loadOriginalVideo', false), - viewerAutoPlayVideo(.appConfig, 'viewer.autoPlayVideo', true), - viewerTapToNavigate(.appConfig, 'viewer.tapToNavigate', false), - - // Network - networkAutoEndpointSwitching(.systemConfig, 'network.autoEndpointSwitching', false), - networkPreferredWifiName(.systemConfig, 'network.preferredWifiName', ''), - networkLocalEndpoint(.systemConfig, 'network.localEndpoint', ''), - networkExternalEndpointList>( - .systemConfig, - 'network.externalEndpointList', - [], - _ListCodec(_PrimitiveCodec.string), - ), - networkCustomHeaders>( - .systemConfig, - 'network.customHeaders', - {}, - _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string), - ), - - // Album - albumSortMode( - .appConfig, - 'album.sortMode', - AlbumSortMode.mostRecent, - _EnumCodec(AlbumSortMode.values), - ), - albumIsReverse(.appConfig, 'album.isReverse', true), - albumIsGrid(.appConfig, 'album.isGrid', false), - - // Backup - backupEnabled(.appConfig, 'backup.enabled', false), - backupUseCellularForVideos(.appConfig, 'backup.useCellularForVideos', false), - backupUseCellularForPhotos(.appConfig, 'backup.useCellularForPhotos', false), - backupRequireCharging(.appConfig, 'backup.requireCharging', false), - backupTriggerDelay(.appConfig, 'backup.triggerDelay', 30), - backupSyncAlbums(.appConfig, 'backup.syncAlbums', false), - - // Timeline - timelineTilesPerRow(.appConfig, 'timeline.tilesPerRow', 4), - timelineGroupAssetsBy( - .appConfig, - 'timeline.groupAssetsBy', - GroupAssetsBy.day, - _EnumCodec(GroupAssetsBy.values), - ), - timelineStorageIndicator(.appConfig, 'timeline.storageIndicator', true), - - // Log - logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)), - - // Map - mapShowFavoriteOnly(.appConfig, 'map.showFavoriteOnly', false), - mapRelativeDate(.appConfig, 'map.relativeDate', 0), - mapIncludeArchived(.appConfig, 'map.includeArchived', false), - mapThemeMode(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)), - mapWithPartners(.appConfig, 'map.withPartners', false), - - // Cleanup - cleanupKeepFavorites(.appConfig, 'cleanup.keepFavorites', true), - cleanupKeepMediaType( - .appConfig, - 'cleanup.keepMediaType', - AssetKeepType.none, - _EnumCodec(AssetKeepType.values), - ), - cleanupKeepAlbumIds>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)), - cleanupCutoffDaysAgo(.appConfig, 'cleanup.cutoffDaysAgo', -1), - cleanupDefaultsInitialized(.appConfig, 'cleanup.defaultsInitialized', false), - - // Slideshow - slideshowTransition(.appConfig, 'slideshow.transition', true), - slideshowRepeat(.appConfig, 'slideshow.repeat', true), - slideshowDuration(.appConfig, 'slideshow.duration', 5), - slideshowLook(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)), - slideshowDirection( - .appConfig, - 'slideshow.direction', - SlideshowDirection.forward, - _EnumCodec(SlideshowDirection.values), - ); - - final MetadataDomain domain; - final String name; - final T defaultValue; - final _MetadataCodec? _codecOverride; - - const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]); - - String get key => '${domain.prefix}.$name'; - - _MetadataCodec get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue); - - String encode(T value) => _codec.encode(value); - - T decode(String raw) => _codec.decode(raw) ?? defaultValue; - - static Map> asKeyMap() => {for (var value in MetadataKey.values) value.key: value}; -} - -sealed class _MetadataCodec { - const _MetadataCodec(); - - String encode(T value); - T? decode(String raw); - - static const Map> _primitives = { - int: _PrimitiveCodec.integer, - double: _PrimitiveCodec.real, - bool: _PrimitiveCodec.boolean, - String: _PrimitiveCodec.string, - DateTime: _DateTimeCodec(), - }; - - static _MetadataCodec forPrimitive(T sample) { - final codec = _primitives[sample.runtimeType]; - if (codec == null) { - throw StateError( - 'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.', - ); - } - return codec as _MetadataCodec; - } -} - -final class _EnumCodec extends _MetadataCodec { - final List values; - - const _EnumCodec(this.values); - - @override - String encode(T value) => value.name; - - @override - T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw); -} - -final class _DateTimeCodec extends _MetadataCodec { - const _DateTimeCodec(); - - @override - String encode(DateTime value) => value.toIso8601String(); - - @override - DateTime? decode(String raw) => DateTime.tryParse(raw); -} - -final class _MapCodec extends _MetadataCodec> { - final _MetadataCodec _keyCodec; - final _MetadataCodec _valueCodec; - - const _MapCodec(this._keyCodec, this._valueCodec); - - @override - String encode(Map value) { - final entries = {}; - value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v)); - return jsonEncode(entries); - } - - @override - Map? decode(String raw) { - try { - final decoded = jsonDecode(raw); - if (decoded is! Map) { - return null; - } - final result = {}; - for (final entry in decoded.entries) { - final rawKey = entry.key; - final rawValue = entry.value; - if (rawKey is! String || rawValue is! String) { - return null; - } - final k = _keyCodec.decode(rawKey); - final v = _valueCodec.decode(rawValue); - if (k == null || v == null) { - return null; - } - result[k] = v; - } - return result; - } on FormatException { - return null; - } - } -} - -final class _ListCodec extends _MetadataCodec> { - final _MetadataCodec _elementCodec; - - const _ListCodec(this._elementCodec); - - @override - String encode(List value) => jsonEncode(value.map(_elementCodec.encode).toList()); - - @override - List? decode(String raw) { - try { - final decoded = jsonDecode(raw); - if (decoded is! List) { - return null; - } - final result = []; - for (final item in decoded) { - if (item is! String) { - return null; - } - final element = _elementCodec.decode(item); - if (element == null) { - return null; - } - result.add(element); - } - return result; - } on FormatException { - return null; - } - } -} - -final class _PrimitiveCodec extends _MetadataCodec { - final T? Function(String) _parse; - - const _PrimitiveCodec._(this._parse); - - @override - String encode(T value) => value.toString(); - - @override - T? decode(String raw) => _parse(raw); - - static const integer = _PrimitiveCodec._(int.tryParse); - static const real = _PrimitiveCodec._(double.tryParse); - static const boolean = _PrimitiveCodec._(bool.tryParse); - static const string = _PrimitiveCodec._(_identity); - - static String? _identity(String s) => s; -} diff --git a/mobile/lib/domain/models/settings_key.dart b/mobile/lib/domain/models/settings_key.dart new file mode 100644 index 0000000000..5277a37a60 --- /dev/null +++ b/mobile/lib/domain/models/settings_key.dart @@ -0,0 +1,217 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; + +enum SettingsKey { + // Theme + themePrimaryColor(codec: _EnumCodec(ImmichColorPreset.values)), + themeMode(codec: _EnumCodec(ThemeMode.values)), + themeDynamic(), + themeColorfulInterface(), + + // Image + imagePreferRemote(), + imageLoadOriginal(), + + // Viewer + viewerLoopVideo(), + viewerLoadOriginalVideo(), + viewerAutoPlayVideo(), + viewerTapToNavigate(), + + // Network + networkAutoEndpointSwitching(), + networkPreferredWifiName(), + networkLocalEndpoint(), + networkExternalEndpointList>(codec: _ListCodec(_PrimitiveCodec.string)), + networkCustomHeaders>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)), + + // Album + albumSortMode(codec: _EnumCodec(AlbumSortMode.values)), + albumIsReverse(), + albumIsGrid(), + + // Backup + backupEnabled(), + backupUseCellularForVideos(), + backupUseCellularForPhotos(), + backupRequireCharging(), + backupTriggerDelay(), + backupSyncAlbums(), + + // Timeline + timelineTilesPerRow(), + timelineGroupAssetsBy(codec: _EnumCodec(GroupAssetsBy.values)), + timelineStorageIndicator(), + + // Log + logLevel(codec: _EnumCodec(LogLevel.values)), + + // Map + mapShowFavoriteOnly(), + mapRelativeDate(), + mapIncludeArchived(), + mapThemeMode(codec: _EnumCodec(ThemeMode.values)), + mapWithPartners(), + + // Cleanup + cleanupKeepFavorites(), + cleanupKeepMediaType(codec: _EnumCodec(AssetKeepType.values)), + cleanupKeepAlbumIds>(codec: _ListCodec(_PrimitiveCodec.string)), + cleanupCutoffDaysAgo(), + cleanupDefaultsInitialized(), + + // Slideshow + slideshowTransition(), + slideshowRepeat(), + slideshowDuration(), + slideshowLook(codec: _EnumCodec(SlideshowLook.values)), + slideshowDirection(codec: _EnumCodec(SlideshowDirection.values)); + + final _SettingsCodec? _codecOverride; + + const SettingsKey({_SettingsCodec? codec}) : _codecOverride = codec; + + _SettingsCodec get _codec => _codecOverride ?? _SettingsCodec.forType(T); + + String encode(T value) => _codec.encode(value); + + T decode(String raw) => _codec.decode(raw); +} + +sealed class _SettingsCodec { + const _SettingsCodec(); + + String encode(T value); + T decode(String raw); + + static const Map> _primitives = { + int: _PrimitiveCodec.integer, + double: _PrimitiveCodec.real, + bool: _PrimitiveCodec.boolean, + String: _PrimitiveCodec.string, + DateTime: _DateTimeCodec(), + }; + + static _SettingsCodec forType(Type runtimeType) { + final codec = _primitives[runtimeType]; + if (codec == null) { + throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.'); + } + return codec as _SettingsCodec; + } +} + +final class _EnumCodec extends _SettingsCodec { + final List values; + + const _EnumCodec(this.values); + + @override + String encode(T value) => value.name; + + @override + T decode(String raw) => values.firstWhere((v) => v.name == raw); +} + +final class _DateTimeCodec extends _SettingsCodec { + const _DateTimeCodec(); + + @override + String encode(DateTime value) => value.toIso8601String(); + + @override + DateTime decode(String raw) => DateTime.parse(raw); +} + +final class _MapCodec extends _SettingsCodec> { + final _SettingsCodec _keyCodec; + final _SettingsCodec _valueCodec; + + const _MapCodec(this._keyCodec, this._valueCodec); + + @override + String encode(Map value) { + final entries = {}; + value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v)); + return jsonEncode(entries); + } + + @override + Map decode(String raw) { + try { + final decoded = jsonDecode(raw); + if (decoded is! Map) { + return {}; + } + final result = {}; + for (final entry in decoded.entries) { + final rawKey = entry.key; + final rawValue = entry.value; + if (rawKey is! String || rawValue is! String) { + return {}; + } + final k = _keyCodec.decode(rawKey); + final v = _valueCodec.decode(rawValue); + result[k] = v; + } + return result; + } on FormatException { + return {}; + } + } +} + +final class _ListCodec extends _SettingsCodec> { + final _SettingsCodec _elementCodec; + + const _ListCodec(this._elementCodec); + + @override + String encode(List value) => jsonEncode(value.map(_elementCodec.encode).toList()); + + @override + List decode(String raw) { + try { + final decoded = jsonDecode(raw); + if (decoded is! List) { + return []; + } + final result = []; + for (final item in decoded) { + if (item is! String) { + return []; + } + final element = _elementCodec.decode(item); + result.add(element); + } + return result; + } on FormatException { + return []; + } + } +} + +final class _PrimitiveCodec extends _SettingsCodec { + final T Function(String) _parse; + + const _PrimitiveCodec._(this._parse); + + @override + String encode(T value) => value.toString(); + + @override + T decode(String raw) => _parse(raw); + + static const integer = _PrimitiveCodec._(int.parse); + static const real = _PrimitiveCodec._(double.parse); + static const boolean = _PrimitiveCodec._(bool.parse); + static const string = _PrimitiveCodec._(_identity); + + static String _identity(String s) => s; +} diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 3411f8aa13..d28f7ff14b 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -39,7 +39,7 @@ class BackgroundWorkerFgService { _foregroundHostApi.saveNotificationMessage(title, body); Future configure({int? minimumDelaySeconds, bool? requireCharging}) { - final backup = MetadataRepository.instance.appConfig.backup; + final backup = SettingsRepository.instance.appConfig.backup; return _foregroundHostApi.configure( BackgroundWorkerSettings( minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay, @@ -67,7 +67,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { BackgroundWorkerFlutterApi.setUp(this); } - bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled; + bool get _isBackupEnabled => SettingsRepository.instance.appConfig.backup.enabled; Future init() async { try { diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 1235d7ac76..216f030b12 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; @@ -12,10 +12,10 @@ import 'package:logging/logging.dart'; /// /// It listens to Dart's [Logger.root], buffers logs in memory (optionally), /// writes them to a persistent [LogRepository], and manages log levels via -/// [MetadataRepository]. +/// [SettingsRepository]. class LogService { final LogRepository _logRepository; - final MetadataRepository _metadataRepository; + final SettingsRepository _settingsRepository; final List _msgBuffer = []; @@ -38,12 +38,12 @@ class LogService { static Future init({ required LogRepository logRepository, - required MetadataRepository metadataRepository, + required SettingsRepository settingsRepository, bool shouldBuffer = true, }) async { _instance ??= await create( logRepository: logRepository, - metadataRepository: metadataRepository, + settingsRepository: settingsRepository, shouldBuffer: shouldBuffer, ); return _instance!; @@ -51,17 +51,17 @@ class LogService { static Future create({ required LogRepository logRepository, - required MetadataRepository metadataRepository, + required SettingsRepository settingsRepository, bool shouldBuffer = true, }) async { - final instance = LogService._(logRepository, metadataRepository, shouldBuffer); + final instance = LogService._(logRepository, settingsRepository, shouldBuffer); await logRepository.truncate(limit: kLogTruncateLimit); - final level = instance._metadataRepository.systemConfig.logLevel; + final level = instance._settingsRepository.appConfig.logLevel; Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO; return instance; } - LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) { + LogService._(this._logRepository, this._settingsRepository, this._shouldBuffer) { _logSubscription = Logger.root.onRecord.listen(_handleLogRecord); } @@ -91,7 +91,7 @@ class LogService { } Future setLogLevel(LogLevel level) async { - await _metadataRepository.write(MetadataKey.logLevel, level); + await _settingsRepository.write(SettingsKey.logLevel, level); Logger.root.level = level.toLevel(); } diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index e873a7631f..35a8f899a8 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -192,43 +192,30 @@ class RemoteAlbumService { required UserDto uploader, required AlbumAssetCandidates candidates, UploadCallbacks uploadCallbacks = const UploadCallbacks(), + Completer? cancelToken, }) async { int addedCount = 0; if (candidates.remoteAssetIds.isNotEmpty) { addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds); } if (candidates.localAssetsToUpload.isNotEmpty) { - addedCount += await _uploadAndAddLocals(albumId, uploader, candidates.localAssetsToUpload, uploadCallbacks); + addedCount += await _uploadAndAddLocals( + albumId, + uploader, + candidates.localAssetsToUpload, + uploadCallbacks, + cancelToken, + ); } return addedCount; } - /// Creates an album, seeding it with already-remote asset IDs, then uploads - /// local-only assets and links each one as it finishes. - Future createAlbumWithAssets({ - required String title, - required UserDto owner, - String? description, - AlbumAssetCandidates candidates = const AlbumAssetCandidates(remoteAssetIds: [], localAssetsToUpload: []), - UploadCallbacks uploadCallbacks = const UploadCallbacks(), - }) async { - final album = await createAlbum( - title: title, - owner: owner, - description: description, - assetIds: candidates.remoteAssetIds, - ); - if (candidates.localAssetsToUpload.isNotEmpty) { - await _uploadAndAddLocals(album.id, owner, candidates.localAssetsToUpload, uploadCallbacks); - } - return album; - } - Future _uploadAndAddLocals( String albumId, UserDto uploader, List localAssets, UploadCallbacks userCallbacks, + Completer? cancelToken, ) async { int addedCount = 0; final pendingAdds = >[]; @@ -258,7 +245,7 @@ class RemoteAlbumService { return; } pendingAdds.add( - _linkUploadedAssetToAlbum(albumId, remoteId, uploader, source) + linkUploadedAssetToAlbum(albumId, remoteId, uploader, source) .then((added) { addedCount += added; }) @@ -269,7 +256,7 @@ class RemoteAlbumService { }, ); - await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks); + await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks, cancelToken: cancelToken); await Future.wait(pendingAdds); return addedCount; } @@ -288,7 +275,7 @@ class RemoteAlbumService { /// `remote_asset_entity` row from the local source so the FK-protected /// junction insert succeeds. Sync overwrites the placeholder later with /// the authoritative server data. - Future _linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async { + Future linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async { final result = await _albumApiRepository.addAssets(albumId, [remoteId]); if (result.added.isEmpty) { return 0; diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index c6324b356e..4cc58b0fe7 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; @@ -39,12 +39,12 @@ enum TimelineOrigin { class TimelineFactory { final DriftTimelineRepository _timelineRepository; - final MetadataRepository _metadataRepository; + final SettingsRepository _settingsRepository; - const TimelineFactory({required this._timelineRepository, required this._metadataRepository}); + const TimelineFactory({required this._timelineRepository, required this._settingsRepository}); GroupAssetsBy get groupBy { - final group = _metadataRepository.appConfig.timeline.groupAssetsBy; + final group = _settingsRepository.appConfig.timeline.groupAssetsBy; // We do not support auto grouping in the new timeline yet, fallback to day grouping return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group; } diff --git a/mobile/lib/infrastructure/entities/metadata.entity.dart b/mobile/lib/infrastructure/entities/settings.entity.dart similarity index 72% rename from mobile/lib/infrastructure/entities/metadata.entity.dart rename to mobile/lib/infrastructure/entities/settings.entity.dart index 2908245040..36e0bcc990 100644 --- a/mobile/lib/infrastructure/entities/metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/settings.entity.dart @@ -1,8 +1,8 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -class MetadataEntity extends Table with DriftDefaultsMixin { - const MetadataEntity(); +class SettingsEntity extends Table with DriftDefaultsMixin { + const SettingsEntity(); TextColumn get key => text()(); @@ -14,5 +14,5 @@ class MetadataEntity extends Table with DriftDefaultsMixin { Set get primaryKey => {key}; @override - String get tableName => "metadata"; + String get tableName => "settings"; } diff --git a/mobile/lib/infrastructure/entities/metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/settings.entity.drift.dart similarity index 74% rename from mobile/lib/infrastructure/entities/metadata.entity.drift.dart rename to mobile/lib/infrastructure/entities/settings.entity.drift.dart index 80bf7bfc43..e2cac89a5e 100644 --- a/mobile/lib/infrastructure/entities/metadata.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/settings.entity.drift.dart @@ -1,28 +1,28 @@ // dart format width=80 // ignore_for_file: type=lint import 'package:drift/drift.dart' as i0; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart' as i1; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart' +import 'package:immich_mobile/infrastructure/entities/settings.entity.dart' as i2; import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; -typedef $$MetadataEntityTableCreateCompanionBuilder = - i1.MetadataEntityCompanion Function({ +typedef $$SettingsEntityTableCreateCompanionBuilder = + i1.SettingsEntityCompanion Function({ required String key, required String value, i0.Value updatedAt, }); -typedef $$MetadataEntityTableUpdateCompanionBuilder = - i1.MetadataEntityCompanion Function({ +typedef $$SettingsEntityTableUpdateCompanionBuilder = + i1.SettingsEntityCompanion Function({ i0.Value key, i0.Value value, i0.Value updatedAt, }); -class $$MetadataEntityTableFilterComposer - extends i0.Composer { - $$MetadataEntityTableFilterComposer({ +class $$SettingsEntityTableFilterComposer + extends i0.Composer { + $$SettingsEntityTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -45,9 +45,9 @@ class $$MetadataEntityTableFilterComposer ); } -class $$MetadataEntityTableOrderingComposer - extends i0.Composer { - $$MetadataEntityTableOrderingComposer({ +class $$SettingsEntityTableOrderingComposer + extends i0.Composer { + $$SettingsEntityTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -70,9 +70,9 @@ class $$MetadataEntityTableOrderingComposer ); } -class $$MetadataEntityTableAnnotationComposer - extends i0.Composer { - $$MetadataEntityTableAnnotationComposer({ +class $$SettingsEntityTableAnnotationComposer + extends i0.Composer { + $$SettingsEntityTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -89,47 +89,47 @@ class $$MetadataEntityTableAnnotationComposer $composableBuilder(column: $table.updatedAt, builder: (column) => column); } -class $$MetadataEntityTableTableManager +class $$SettingsEntityTableTableManager extends i0.RootTableManager< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData, - i1.$$MetadataEntityTableFilterComposer, - i1.$$MetadataEntityTableOrderingComposer, - i1.$$MetadataEntityTableAnnotationComposer, - $$MetadataEntityTableCreateCompanionBuilder, - $$MetadataEntityTableUpdateCompanionBuilder, + i1.$SettingsEntityTable, + i1.SettingsEntityData, + i1.$$SettingsEntityTableFilterComposer, + i1.$$SettingsEntityTableOrderingComposer, + i1.$$SettingsEntityTableAnnotationComposer, + $$SettingsEntityTableCreateCompanionBuilder, + $$SettingsEntityTableUpdateCompanionBuilder, ( - i1.MetadataEntityData, + i1.SettingsEntityData, i0.BaseReferences< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData + i1.$SettingsEntityTable, + i1.SettingsEntityData >, ), - i1.MetadataEntityData, + i1.SettingsEntityData, i0.PrefetchHooks Function() > { - $$MetadataEntityTableTableManager( + $$SettingsEntityTableTableManager( i0.GeneratedDatabase db, - i1.$MetadataEntityTable table, + i1.$SettingsEntityTable table, ) : super( i0.TableManagerState( db: db, table: table, createFilteringComposer: () => - i1.$$MetadataEntityTableFilterComposer($db: db, $table: table), + i1.$$SettingsEntityTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table), + i1.$$SettingsEntityTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => i1 - .$$MetadataEntityTableAnnotationComposer($db: db, $table: table), + .$$SettingsEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ i0.Value key = const i0.Value.absent(), i0.Value value = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), - }) => i1.MetadataEntityCompanion( + }) => i1.SettingsEntityCompanion( key: key, value: value, updatedAt: updatedAt, @@ -139,7 +139,7 @@ class $$MetadataEntityTableTableManager required String key, required String value, i0.Value updatedAt = const i0.Value.absent(), - }) => i1.MetadataEntityCompanion.insert( + }) => i1.SettingsEntityCompanion.insert( key: key, value: value, updatedAt: updatedAt, @@ -152,34 +152,34 @@ class $$MetadataEntityTableTableManager ); } -typedef $$MetadataEntityTableProcessedTableManager = +typedef $$SettingsEntityTableProcessedTableManager = i0.ProcessedTableManager< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData, - i1.$$MetadataEntityTableFilterComposer, - i1.$$MetadataEntityTableOrderingComposer, - i1.$$MetadataEntityTableAnnotationComposer, - $$MetadataEntityTableCreateCompanionBuilder, - $$MetadataEntityTableUpdateCompanionBuilder, + i1.$SettingsEntityTable, + i1.SettingsEntityData, + i1.$$SettingsEntityTableFilterComposer, + i1.$$SettingsEntityTableOrderingComposer, + i1.$$SettingsEntityTableAnnotationComposer, + $$SettingsEntityTableCreateCompanionBuilder, + $$SettingsEntityTableUpdateCompanionBuilder, ( - i1.MetadataEntityData, + i1.SettingsEntityData, i0.BaseReferences< i0.GeneratedDatabase, - i1.$MetadataEntityTable, - i1.MetadataEntityData + i1.$SettingsEntityTable, + i1.SettingsEntityData >, ), - i1.MetadataEntityData, + i1.SettingsEntityData, i0.PrefetchHooks Function() >; -class $MetadataEntityTable extends i2.MetadataEntity - with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> { +class $SettingsEntityTable extends i2.SettingsEntity + with i0.TableInfo<$SettingsEntityTable, i1.SettingsEntityData> { @override final i0.GeneratedDatabase attachedDatabase; final String? _alias; - $MetadataEntityTable(this.attachedDatabase, [this._alias]); + $SettingsEntityTable(this.attachedDatabase, [this._alias]); static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key'); @override late final i0.GeneratedColumn key = i0.GeneratedColumn( @@ -219,10 +219,10 @@ class $MetadataEntityTable extends i2.MetadataEntity String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'metadata'; + static const String $name = 'settings'; @override i0.VerificationContext validateIntegrity( - i0.Insertable instance, { + i0.Insertable instance, { bool isInserting = false, }) { final context = i0.VerificationContext(); @@ -255,9 +255,9 @@ class $MetadataEntityTable extends i2.MetadataEntity @override Set get $primaryKey => {key}; @override - i1.MetadataEntityData map(Map data, {String? tablePrefix}) { + i1.SettingsEntityData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return i1.MetadataEntityData( + return i1.SettingsEntityData( key: attachedDatabase.typeMapping.read( i0.DriftSqlType.string, data['${effectivePrefix}key'], @@ -274,8 +274,8 @@ class $MetadataEntityTable extends i2.MetadataEntity } @override - $MetadataEntityTable createAlias(String alias) { - return $MetadataEntityTable(attachedDatabase, alias); + $SettingsEntityTable createAlias(String alias) { + return $SettingsEntityTable(attachedDatabase, alias); } @override @@ -284,12 +284,12 @@ class $MetadataEntityTable extends i2.MetadataEntity bool get isStrict => true; } -class MetadataEntityData extends i0.DataClass - implements i0.Insertable { +class SettingsEntityData extends i0.DataClass + implements i0.Insertable { final String key; final String value; final DateTime updatedAt; - const MetadataEntityData({ + const SettingsEntityData({ required this.key, required this.value, required this.updatedAt, @@ -303,12 +303,12 @@ class MetadataEntityData extends i0.DataClass return map; } - factory MetadataEntityData.fromJson( + factory SettingsEntityData.fromJson( Map json, { i0.ValueSerializer? serializer, }) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; - return MetadataEntityData( + return SettingsEntityData( key: serializer.fromJson(json['key']), value: serializer.fromJson(json['value']), updatedAt: serializer.fromJson(json['updatedAt']), @@ -324,17 +324,17 @@ class MetadataEntityData extends i0.DataClass }; } - i1.MetadataEntityData copyWith({ + i1.SettingsEntityData copyWith({ String? key, String? value, DateTime? updatedAt, - }) => i1.MetadataEntityData( + }) => i1.SettingsEntityData( key: key ?? this.key, value: value ?? this.value, updatedAt: updatedAt ?? this.updatedAt, ); - MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) { - return MetadataEntityData( + SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) { + return SettingsEntityData( key: data.key.present ? data.key.value : this.key, value: data.value.present ? data.value.value : this.value, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, @@ -343,7 +343,7 @@ class MetadataEntityData extends i0.DataClass @override String toString() { - return (StringBuffer('MetadataEntityData(') + return (StringBuffer('SettingsEntityData(') ..write('key: $key, ') ..write('value: $value, ') ..write('updatedAt: $updatedAt') @@ -356,29 +356,29 @@ class MetadataEntityData extends i0.DataClass @override bool operator ==(Object other) => identical(this, other) || - (other is i1.MetadataEntityData && + (other is i1.SettingsEntityData && other.key == this.key && other.value == this.value && other.updatedAt == this.updatedAt); } -class MetadataEntityCompanion - extends i0.UpdateCompanion { +class SettingsEntityCompanion + extends i0.UpdateCompanion { final i0.Value key; final i0.Value value; final i0.Value updatedAt; - const MetadataEntityCompanion({ + const SettingsEntityCompanion({ this.key = const i0.Value.absent(), this.value = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), }); - MetadataEntityCompanion.insert({ + SettingsEntityCompanion.insert({ required String key, required String value, this.updatedAt = const i0.Value.absent(), }) : key = i0.Value(key), value = i0.Value(value); - static i0.Insertable custom({ + static i0.Insertable custom({ i0.Expression? key, i0.Expression? value, i0.Expression? updatedAt, @@ -390,12 +390,12 @@ class MetadataEntityCompanion }); } - i1.MetadataEntityCompanion copyWith({ + i1.SettingsEntityCompanion copyWith({ i0.Value? key, i0.Value? value, i0.Value? updatedAt, }) { - return i1.MetadataEntityCompanion( + return i1.SettingsEntityCompanion( key: key ?? this.key, value: value ?? this.value, updatedAt: updatedAt ?? this.updatedAt, @@ -419,7 +419,7 @@ class MetadataEntityCompanion @override String toString() { - return (StringBuffer('MetadataEntityCompanion(') + return (StringBuffer('SettingsEntityCompanion(') ..write('key: $key, ') ..write('value: $value, ') ..write('updatedAt: $updatedAt') diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index e81fe58ba9..6bb2e946f1 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -13,7 +13,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; @@ -55,7 +55,7 @@ import 'package:logging/logging.dart'; StoreEntity, TrashedLocalAssetEntity, AssetEditEntity, - MetadataEntity, + SettingsEntity, ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) @@ -98,7 +98,7 @@ class Drift extends $Drift { } @override - int get schemaVersion => 26; + int get schemaVersion => 27; @override MigrationStrategy get migration => MigrationStrategy( @@ -276,6 +276,9 @@ class Drift extends $Drift { from25To26: (m, v26) async { await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt); }, + from26To27: (m, v27) async { + await customStatement('ALTER TABLE metadata RENAME TO settings'); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index c43a83f86a..692523219b 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -43,7 +43,7 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity as i20; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart' as i21; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart' as i22; import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' as i23; @@ -91,7 +91,7 @@ abstract class $Drift extends i0.GeneratedDatabase { .$TrashedLocalAssetEntityTable(this); late final i21.$AssetEditEntityTable assetEditEntity = i21 .$AssetEditEntityTable(this); - late final i22.$MetadataEntityTable metadataEntity = i22.$MetadataEntityTable( + late final i22.$SettingsEntityTable settingsEntity = i22.$SettingsEntityTable( this, ); i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer( @@ -132,7 +132,7 @@ abstract class $Drift extends i0.GeneratedDatabase { storeEntity, trashedLocalAssetEntity, assetEditEntity, - metadataEntity, + settingsEntity, i10.idxPartnerSharedWithId, i11.idxLatLng, i11.idxRemoteExifCity, @@ -395,6 +395,6 @@ class $DriftManager { ); i21.$$AssetEditEntityTableTableManager get assetEditEntity => i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity); - i22.$$MetadataEntityTableTableManager get metadataEntity => - i22.$$MetadataEntityTableTableManager(_db, _db.metadataEntity); + i22.$$SettingsEntityTableTableManager get settingsEntity => + i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 1fb88de1d0..a51174d980 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -13539,6 +13539,550 @@ i1.GeneratedColumn _column_212(String aliasedName) => type: i1.DriftSqlType.string, $customConstraints: 'NULL', ); + +final class Schema27 extends i0.VersionedSchema { + Schema27({required super.database}) : super(version: 27); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetOwnerVisibilityDeletedCreated, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + assetEditEntity, + settings, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteExifCity, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxAssetFaceVisiblePerson, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + idxAssetEditAssetId, + ]; + late final Shape33 userEntity = Shape33( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_109, + _column_110, + _column_111, + _column_112, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape50 remoteAssetEntity = Shape50( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_119, + _column_120, + _column_121, + _column_122, + _column_123, + _column_124, + _column_212, + _column_125, + _column_126, + _column_127, + _column_128, + _column_129, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape35 stackEntity = Shape35( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_121, + _column_130, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape36 localAssetEntity = Shape36( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_131, + _column_120, + _column_132, + _column_133, + _column_134, + _column_135, + _column_136, + _column_137, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape48 remoteAlbumEntity = Shape48( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_138, + _column_114, + _column_115, + _column_139, + _column_140, + _column_141, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape38 localAlbumEntity = Shape38( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_115, + _column_142, + _column_143, + _column_144, + _column_145, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape39 localAlbumAssetEntity = Shape39( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_146, _column_147, _column_145], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index( + 'idx_remote_asset_owner_visibility_deleted_created', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)', + ); + late final Shape40 authUserEntity = Shape40( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_109, + _column_148, + _column_110, + _column_111, + _column_149, + _column_150, + _column_151, + _column_152, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_153, _column_154, _column_155], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape41 partnerEntity = Shape41( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_156, _column_157, _column_158], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape42 remoteExifEntity = Shape42( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_159, + _column_160, + _column_161, + _column_162, + _column_163, + _column_164, + _column_117, + _column_116, + _column_165, + _column_166, + _column_167, + _column_168, + _column_135, + _column_136, + _column_169, + _column_170, + _column_171, + _column_172, + _column_173, + _column_174, + _column_175, + _column_176, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_159, _column_177], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_177, _column_153, _column_178], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape43 remoteAssetCloudIdEntity = Shape43( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_159, + _column_179, + _column_180, + _column_134, + _column_135, + _column_136, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape44 memoryEntity = Shape44( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_124, + _column_121, + _column_113, + _column_181, + _column_182, + _column_183, + _column_184, + _column_185, + _column_186, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_159, _column_187], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape45 personEntity = Shape45( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_121, + _column_108, + _column_188, + _column_189, + _column_190, + _column_191, + _column_192, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape46 assetFaceEntity = Shape46( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_159, + _column_193, + _column_194, + _column_195, + _column_196, + _column_197, + _column_198, + _column_199, + _column_200, + _column_201, + _column_124, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_202, _column_203, _column_204], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape47 trashedLocalAssetEntity = Shape47( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_205, + _column_131, + _column_120, + _column_132, + _column_206, + _column_137, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape32 assetEditEntity = Shape32( + source: i0.VersionedTable( + entityName: 'asset_edit_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_159, + _column_207, + _column_208, + _column_209, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape49 settings = Shape49( + source: i0.VersionedTable( + entityName: 'settings', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_210, _column_211, _column_115], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteExifCity = i1.Index( + 'idx_remote_exif_city', + 'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxAssetFaceVisiblePerson = i1.Index( + 'idx_asset_face_visible_person', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + final i1.Index idxAssetEditAssetId = i1.Index( + 'idx_asset_edit_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -13565,6 +14109,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema24 schema) from23To24, required Future Function(i1.Migrator m, Schema25 schema) from24To25, required Future Function(i1.Migrator m, Schema26 schema) from25To26, + required Future Function(i1.Migrator m, Schema27 schema) from26To27, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -13693,6 +14238,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from25To26(migrator, schema); return 26; + case 26: + final schema = Schema27(database: database); + final migrator = i1.Migrator(database, schema); + await from26To27(migrator, schema); + return 27; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -13725,6 +14275,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema24 schema) from23To24, required Future Function(i1.Migrator m, Schema25 schema) from24To25, required Future Function(i1.Migrator m, Schema26 schema) from25To26, + required Future Function(i1.Migrator m, Schema27 schema) from26To27, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -13752,5 +14303,6 @@ i1.OnUpgrade stepByStep({ from23To24: from23To24, from24To25: from24To25, from25To26: from25To26, + from26To27: from26To27, ), ); diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart deleted file mode 100644 index fa1d275026..0000000000 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/domain/models/config/system_config.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; - -class MetadataRepository extends DriftDatabaseRepository { - final Drift _db; - final Map _cache = {}; - - MetadataRepository._(this._db) : super(_db); - - static MetadataRepository? _instance; - - static MetadataRepository get instance { - final instance = _instance; - if (instance == null) { - throw StateError('MetadataRepository not initialized. Call ensureInitialized() first'); - } - return instance; - } - - AppConfig _appConfig = const .new(); - AppConfig get appConfig => _appConfig; - - SystemConfig _systemConfig = const .new(); - SystemConfig get systemConfig => _systemConfig; - - static Future ensureInitialized(Drift db) async { - if (_instance == null) { - final instance = MetadataRepository._(db); - await instance._hydrate(); - _instance = instance; - } - return _instance!; - } - - static Future refresh() async { - instance._cache.clear(); - instance._appConfig = const .new(); - instance._systemConfig = const .new(); - await instance._hydrate(); - } - - Future _hydrate() async => _hydrateCache(await _db.select(_db.metadataEntity).get()); - - T _read(MetadataKey key) => (_cache[key] as T?) ?? key.defaultValue; - - Future write(MetadataKey key, U value) async { - if (_read(key) == value) { - return; - } - - await _db - .into(_db.metadataEntity) - .insertOnConflictUpdate( - MetadataEntityCompanion.insert(key: key.key, value: key.encode(value), updatedAt: Value(DateTime.now())), - ); - _updateCache(key, value); - } - - Future delete(MetadataKey key) async { - await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go(); - _updateCache(key, key.defaultValue); - } - - Stream watchAppConfig() => _watchDomain(.appConfig).distinct(); - - Stream watchSystemConfig() => _watchDomain(.systemConfig).distinct(); - - Stream _watchDomain(MetadataDomain domain) { - final query = _db.select(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%')); - return query.watch().map((rows) { - _hydrateCache(rows); - return domain.config(this); - }); - } - - void _hydrateCache(List rows) { - final keyMap = MetadataKey.asKeyMap(); - for (final row in rows) { - final key = keyMap[row.key]; - if (key == null) { - continue; - } - _updateCache(key, key.decode(row.value)); - } - } - - void _updateCache(MetadataKey key, T value) { - if (_cache[key] == value) { - return; - } - _cache[key] = value; - key.domain.rebuild(this); - } -} - -extension on MetadataDomain { - T config(MetadataRepository repo) => switch (this) { - .appConfig => repo._appConfig as T, - .systemConfig => repo._systemConfig as T, - }; - - void rebuild(MetadataRepository repo) { - switch (this) { - case .appConfig: - repo._appConfig = .new( - theme: .new( - mode: repo._read(.themeMode), - primaryColor: repo._read(.themePrimaryColor), - dynamicTheme: repo._read(.themeDynamic), - colorfulInterface: repo._read(.themeColorfulInterface), - ), - cleanup: .new( - keepFavorites: repo._read(.cleanupKeepFavorites), - keepMediaType: repo._read(.cleanupKeepMediaType), - keepAlbumIds: repo._read(.cleanupKeepAlbumIds), - cutoffDaysAgo: repo._read(.cleanupCutoffDaysAgo), - defaultsInitialized: repo._read(.cleanupDefaultsInitialized), - ), - map: .new( - relativeDays: repo._read(.mapRelativeDate), - favoritesOnly: repo._read(.mapShowFavoriteOnly), - includeArchived: repo._read(.mapIncludeArchived), - themeMode: repo._read(.mapThemeMode), - withPartners: repo._read(.mapWithPartners), - ), - timeline: .new( - tilesPerRow: repo._read(.timelineTilesPerRow), - groupAssetsBy: repo._read(.timelineGroupAssetsBy), - storageIndicator: repo._read(.timelineStorageIndicator), - ), - image: .new(preferRemote: repo._read(.imagePreferRemote), loadOriginal: repo._read(.imageLoadOriginal)), - viewer: .new( - loopVideo: repo._read(.viewerLoopVideo), - loadOriginalVideo: repo._read(.viewerLoadOriginalVideo), - autoPlayVideo: repo._read(.viewerAutoPlayVideo), - tapToNavigate: repo._read(.viewerTapToNavigate), - ), - slideshow: .new( - transition: repo._read(.slideshowTransition), - repeat: repo._read(.slideshowRepeat), - duration: repo._read(.slideshowDuration), - look: repo._read(.slideshowLook), - direction: repo._read(.slideshowDirection), - ), - album: .new( - sortMode: repo._read(.albumSortMode), - isReverse: repo._read(.albumIsReverse), - isGrid: repo._read(.albumIsGrid), - ), - backup: .new( - enabled: repo._read(.backupEnabled), - useCellularForVideos: repo._read(.backupUseCellularForVideos), - useCellularForPhotos: repo._read(.backupUseCellularForPhotos), - requireCharging: repo._read(.backupRequireCharging), - triggerDelay: repo._read(.backupTriggerDelay), - syncAlbums: repo._read(.backupSyncAlbums), - ), - ); - case .systemConfig: - repo._systemConfig = .new( - logLevel: repo._read(.logLevel), - network: .new( - autoEndpointSwitching: repo._read(.networkAutoEndpointSwitching), - preferredWifiName: repo._read(.networkPreferredWifiName).nullIfEmpty, - localEndpoint: repo._read(.networkLocalEndpoint).nullIfEmpty, - externalEndpointList: repo._read(.networkExternalEndpointList), - customHeaders: repo._read(.networkCustomHeaders), - ), - ); - } - } -} diff --git a/mobile/lib/infrastructure/repositories/settings.repository.dart b/mobile/lib/infrastructure/repositories/settings.repository.dart new file mode 100644 index 0000000000..56066f543a --- /dev/null +++ b/mobile/lib/infrastructure/repositories/settings.repository.dart @@ -0,0 +1,84 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class SettingsRepository extends DriftDatabaseRepository { + final Drift _db; + + SettingsRepository._(this._db) : super(_db); + + static SettingsRepository? _instance; + + static SettingsRepository get instance { + final instance = _instance; + if (instance == null) { + throw StateError('SettingsRepository not initialized. Call ensureInitialized() first'); + } + return instance; + } + + AppConfig _appConfig = const .new(); + AppConfig get appConfig => _appConfig; + + static Future ensureInitialized(Drift db) async { + if (_instance == null) { + final instance = SettingsRepository._(db); + await instance.refresh(); + _instance = instance; + } + return _instance!; + } + + Future refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get()); + + Future clear(Iterable keys) async { + if (keys.isEmpty) { + return; + } + + final names = keys.map((key) => key.name).toList(); + await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go(); + + for (final key in keys) { + _appConfig = _appConfig.write(key, defaultConfig.read(key)); + } + } + + Future write(SettingsKey key, U value) async { + if (value == _appConfig.read(key)) { + return; + } + + if (value == defaultConfig.read(key)) { + return clear([key]); + } + + await _db + .into(_db.settingsEntity) + .insertOnConflictUpdate( + SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())), + ); + _appConfig = _appConfig.write(key, value); + } + + Stream watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) { + _applyOverrides(rows); + return _appConfig; + }); + + void _applyOverrides(List rows) { + _appConfig = AppConfig.fromEntries( + rows.fold({}, (overrides, row) { + final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key); + if (metadataKey == null) { + return overrides; + } + + return {...overrides, metadataKey: metadataKey.decode(row.value)}; + }), + ); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 19455be61c..cc5f131572 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -25,7 +25,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; diff --git a/mobile/lib/models/server_info/server_version.model.dart b/mobile/lib/models/server_info/server_version.model.dart index c8bf73db81..40f35a3cd0 100644 --- a/mobile/lib/models/server_info/server_version.model.dart +++ b/mobile/lib/models/server_info/server_version.model.dart @@ -2,16 +2,12 @@ import 'package:immich_mobile/utils/semver.dart'; import 'package:openapi/api.dart'; class ServerVersion extends SemVer { - const ServerVersion({required super.major, required super.minor, required super.patch}); + const ServerVersion({required super.major, required super.minor, required super.patch, super.prerelease}); - @override - String toString() { - return 'ServerVersion(major: $major, minor: $minor, patch: $patch)'; - } + ServerVersion.fromDto(ServerVersionResponseDto dto) + : super(major: dto.major, minor: dto.minor, patch: dto.patch_, prerelease: dto.prerelease); - ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_); - - bool isAtLeast({int major = 0, int minor = 0, int patch = 0}) { - return this >= SemVer(major: major, minor: minor, patch: patch); + bool isAtLeast({int major = 0, int minor = 0, int patch = 0, int? prerelease}) { + return this >= SemVer(major: major, minor: minor, patch: patch, prerelease: prerelease); } } diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index de37437326..f999e7671f 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -8,11 +8,11 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; @@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); @@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState a.backupSelection == BackupSelection.selected) @@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index 4e8a185955..42643a0496 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -4,10 +4,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:logging/logging.dart'; @@ -19,7 +19,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { bool hasPopped = false; - final previousBackup = ref.read(metadataProvider).appConfig.backup; + final previousBackup = ref.read(appConfigProvider).backup; final previousCellularForVideos = previousBackup.useCellularForVideos; final previousCellularForPhotos = previousBackup.useCellularForPhotos; return PopScope( @@ -27,7 +27,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { // There is an issue with Flutter where the pop event // can be triggered multiple times, so we guard it with _hasPopped - final currentBackup = ref.read(metadataProvider).appConfig.backup; + final currentBackup = ref.read(appConfigProvider).backup; final currentCellularForVideos = currentBackup.useCellularForVideos; final currentCellularForPhotos = currentBackup.useCellularForPhotos; @@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { } await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); - final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled; + final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled; if (!isBackupEnabled) { return; } diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index e599286dcf..9a6b602b04 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -3,10 +3,9 @@ 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/metadata_key.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; class SettingsHeader { String key = ""; @@ -22,7 +21,7 @@ class HeaderSettingsPage extends HookConsumerWidget { final headers = useState>([]); final setInitialHeaders = useState(false); - final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders; + final storedHeaders = ref.read(appConfigProvider).network.customHeaders; if (!setInitialHeaders.value) { storedHeaders.forEach((k, v) { final header = SettingsHeader(); @@ -94,7 +93,7 @@ class HeaderSettingsPage extends HookConsumerWidget { headersMap[key] = value; } - await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap); + await ref.read(settingsProvider).write(.networkCustomHeaders, headersMap); await ref.read(apiServiceProvider).updateHeaders(); } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 7b49d98307..aaa9fffc05 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -7,12 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; @@ -36,7 +36,7 @@ class BootstrapErrorWidget extends StatelessWidget { @override Widget build(BuildContext _) { - final immichTheme = MetadataKey.themePrimaryColor.defaultValue.themeOfPreset; + final immichTheme = defaultConfig.theme.primaryColor.themeOfPreset; return EasyLocalization( supportedLocales: locales.values.toList(), @@ -341,7 +341,7 @@ class SplashScreenPageState extends ConsumerState { await backgroundManager.hashAssets(); } - if (MetadataRepository.instance.appConfig.backup.syncAlbums) { + if (SettingsRepository.instance.appConfig.backup.syncAlbums) { await backgroundManager.syncLinkedAlbum(); } } catch (e) { @@ -370,7 +370,7 @@ class SplashScreenPageState extends ConsumerState { } Future _resumeBackup(DriftBackupNotifier notifier) async { - final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled; + final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled; if (isEnableBackup) { final currentUser = Store.tryGet(StoreKey.currentUser); diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 4c4ee48cf9..3a5f95554c 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -17,7 +17,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.wid import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index ecfe4a60fe..1ab3f2039d 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -142,13 +143,18 @@ class _AddActionButtonState extends ConsumerState { return; } - final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(album.id, [latest.remoteId!]); + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album); if (!context.mounted) { return; } - if (addedCount == 0) { + if (!result.success) { + ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + return; + } + + if (result.count == 0) { ImmichToast.show( context: context, msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}), @@ -159,7 +165,7 @@ class _AddActionButtonState extends ConsumerState { msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), ); - // Invalidate using the asset's remote ID to refresh the "Appears in" list + // Refresh the "Appears in" list on the asset's info panel. ref.invalidate(albumsContainingAssetProvider(latest.remoteId!)); } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart index 2e15de49df..6911d09f89 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; /// This delete action has the following behavior: /// - Prompt to delete the asset locally @@ -39,6 +40,8 @@ class DeleteLocalActionButton extends ConsumerWidget { return; } + ref.invalidate(localAlbumProvider); + final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()}); if (context.mounted) { diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index 6fbd6f7dfa..7bc5dacb16 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -14,7 +14,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class _SharePreparingDialog extends StatelessWidget { - const _SharePreparingDialog(); + final ValueNotifier progress; + + const _SharePreparingDialog({required this.progress}); @override Widget build(BuildContext context) { @@ -22,8 +24,24 @@ class _SharePreparingDialog extends StatelessWidget { content: Column( mainAxisSize: MainAxisSize.min, children: [ - const CircularProgressIndicator(), - Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()), + Container(margin: const EdgeInsets.only(bottom: 12), child: const Text('share_dialog_preparing').tr()), + SizedBox( + width: 240, + child: ValueListenableBuilder( + valueListenable: progress, + builder: (context, value, _) { + final percent = value == null ? null : (value * 100).clamp(0, 100); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearProgressIndicator(value: value, minHeight: 8.0), + if (percent != null) + Container(margin: const EdgeInsets.only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')), + ], + ); + }, + ), + ), ], ), ); @@ -43,32 +61,39 @@ class ShareActionButton extends ConsumerWidget { } final cancelCompleter = Completer(); - const preparingDialog = _SharePreparingDialog(); + final progress = ValueNotifier(null); + final preparingDialog = _SharePreparingDialog(progress: progress); await showDialog( context: context, builder: (BuildContext buildContext) { - ref.read(actionProvider.notifier).shareAssets(source, context, cancelCompleter: cancelCompleter).then(( - ActionResult result, - ) { - if (cancelCompleter.isCompleted || !context.mounted) { - return; - } + ref + .read(actionProvider.notifier) + .shareAssets( + source, + context, + cancelCompleter: cancelCompleter, + onAssetDownloadProgress: (value) => progress.value = value, + ) + .then((ActionResult result) { + if (cancelCompleter.isCompleted || !context.mounted) { + return; + } - ref.read(multiSelectProvider.notifier).reset(); + ref.read(multiSelectProvider.notifier).reset(); - if (!result.success) { - ImmichToast.show( - context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } + if (!result.success) { + ImmichToast.show( + context: context, + msg: 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } - buildContext.pop(); - }); + buildContext.pop(); + }); - // show a loading spinner with a "Preparing" message + // Show download progress with a "Preparing" message return preparingDialog; }, barrierDismissible: false, @@ -77,6 +102,7 @@ class ShareActionButton extends ConsumerWidget { if (!cancelCompleter.isCompleted) { cancelCompleter.complete(); } + progress.dispose(); }); } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 6241623978..5a174bfc5e 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -15,12 +14,11 @@ import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -58,7 +56,7 @@ class _AlbumSelectorState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - final albumConfig = ref.read(metadataProvider).appConfig.album; + final albumConfig = ref.read(appConfigProvider).album; setState(() { sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse); @@ -94,7 +92,7 @@ class _AlbumSelectorState extends ConsumerState { setState(() { isGrid = !isGrid; }); - ref.read(metadataProvider).write(MetadataKey.albumIsGrid, isGrid); + ref.read(settingsProvider).write(.albumIsGrid, isGrid); } void changeFilter(QuickFilterMode mode) { @@ -110,9 +108,9 @@ class _AlbumSelectorState extends ConsumerState { this.sort = sort; }); - final metadata = ref.read(metadataProvider); - await metadata.write(MetadataKey.albumSortMode, sort.mode); - await metadata.write(MetadataKey.albumIsReverse, sort.isReverse); + final metadata = ref.read(settingsProvider); + await metadata.write(.albumSortMode, sort.mode); + await metadata.write(.albumIsReverse, sort.isReverse); await sortAlbums(); } @@ -747,12 +745,10 @@ class AddToAlbumHeader extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Future onCreateAlbum() async { + final selectedAssets = ref.read(multiSelectProvider).selectedAssets; final newAlbum = await ref .read(remoteAlbumProvider.notifier) - .createAlbum( - title: "Untitled Album", - assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(), - ); + .createAlbumWithAssets(title: "Untitled Album", assets: selectedAssets); if (newAlbum == null) { ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); diff --git a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart index 397170ec54..2701316e75 100644 --- a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart +++ b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; /// Pinned banner sliver that surfaces in-flight album uploads directly under /// the album app bar. Renders nothing while the queue is empty. Tapping the @@ -165,6 +166,8 @@ class _PendingUploadsSheet extends ConsumerWidget { } final failedCount = pending.where((p) => p.failed).length; + final inFlightCount = pending.length - failedCount; + final canAbort = inFlightCount > 0 && ref.watch(manualUploadCancelTokenProvider) != null; return SafeArea( child: Padding( @@ -183,7 +186,21 @@ class _PendingUploadsSheet extends ConsumerWidget { style: context.textTheme.titleMedium, ), ), - if (failedCount > 0) + if (canAbort) + TextButton.icon( + onPressed: () { + final cancelToken = ref.read(manualUploadCancelTokenProvider); + if (cancelToken != null && !cancelToken.isCompleted) { + cancelToken.complete(); + } + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + ref.read(pendingAlbumUploadsProvider(albumId).notifier).clear(); + }, + icon: const Icon(Icons.stop_circle_outlined, size: 18), + label: Text('cancel'.t(context: context)), + style: TextButton.styleFrom(foregroundColor: context.colorScheme.error), + ) + else if (failedCount > 0) TextButton.icon( onPressed: () => ref.read(pendingAlbumUploadsProvider(albumId).notifier).clearFailed(), icon: const Icon(Icons.clear_rounded, size: 18), diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index 0afc1b781c..84edc4df65 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -19,7 +19,7 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; @@ -241,7 +241,7 @@ class _AssetPageState extends ConsumerState { return; } - final tapToNavigate = ref.read(metadataProvider).appConfig.viewer.tapToNavigate; + final tapToNavigate = ref.read(appConfigProvider).viewer.tapToNavigate; if (!tapToNavigate) { _viewer.toggleControls(); return; diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 97ca8ace10..c1e6fe10e6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; @@ -128,7 +128,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg final remoteId = (videoAsset as RemoteAsset).id; final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref.read(metadataProvider).appConfig.viewer.loadOriginalVideo; + final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo; final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; final String videoUrl = videoAsset.livePhotoVideoId != null ? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl' @@ -161,7 +161,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg return; } - final autoPlayVideo = ref.read(metadataProvider).appConfig.viewer.autoPlayVideo; + final autoPlayVideo = ref.read(appConfigProvider).viewer.autoPlayVideo; if (autoPlayVideo || widget.asset.isMotionPhoto) { await _notifier.play(); } @@ -212,7 +212,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg } await _notifier.load(source); - final loopVideo = ref.read(metadataProvider).appConfig.viewer.loopVideo; + final loopVideo = ref.read(appConfigProvider).viewer.loopVideo; await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo); await _notifier.setVolume(1); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart index 5a79485daf..418d41e1f2 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; @@ -34,7 +33,7 @@ class ViewerKebabMenu extends ConsumerWidget { final isInLockedView = ref.watch(inLockedViewProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider); final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; - final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); + final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); final actionContext = ActionButtonContext( asset: asset, diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index 708d3a9879..3bf48783ea 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; class BackupToggleButton extends ConsumerStatefulWidget { final VoidCallback onStart; @@ -31,7 +30,7 @@ class BackupToggleButtonState extends ConsumerState with Sin end: 1, ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); - _isEnabled = ref.read(metadataProvider).appConfig.backup.enabled; + _isEnabled = ref.read(appConfigProvider).backup.enabled; } @override @@ -41,7 +40,7 @@ class BackupToggleButtonState extends ConsumerState with Sin } Future _onToggle(bool value) async { - await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value); + await ref.read(settingsProvider).write(.backupEnabled, value); setState(() { _isEnabled = value; diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 0bafacfe54..c3a569407a 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -3,9 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; @@ -25,7 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -63,37 +61,23 @@ class _GeneralBottomSheetState extends ConsumerState { userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), ); - Future addAssetsToAlbum(RemoteAlbum album) async { - final selectedAssets = multiselect.selectedAssets; - if (selectedAssets.isEmpty) { + Future addToAlbum(RemoteAlbum album) async { + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); + + if (!context.mounted) { return; } - final remoteAssets = selectedAssets.whereType(); - final addedCount = await ref - .read(remoteAlbumProvider.notifier) - .addAssets(album.id, remoteAssets.map((e) => e.id).toList()); - - if (selectedAssets.length != remoteAssets.length) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context), - ); + if (!result.success) { + ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + return; } - - if (addedCount != remoteAssets.length) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}), - ); - } - - ref.read(multiSelectProvider.notifier).reset(); + ImmichToast.show( + context: context, + msg: result.count == 0 + ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) + : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ); } Future onKeyboardExpand() { @@ -131,12 +115,10 @@ class _GeneralBottomSheetState extends ConsumerState { const DeleteLocalActionButton(source: ActionSource.timeline), if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline), ], - slivers: multiselect.hasRemote - ? [ - const AddToAlbumHeader(), - AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), - ] - : [], + slivers: [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand), + ], ); } } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index b1e87dfaea..ac8c77af03 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -1,25 +1,78 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; -class LocalAlbumBottomSheet extends ConsumerWidget { +class LocalAlbumBottomSheet extends ConsumerStatefulWidget { const LocalAlbumBottomSheet({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - return const BaseBottomSheet( + ConsumerState createState() => _LocalAlbumBottomSheetState(); +} + +class _LocalAlbumBottomSheetState extends ConsumerState { + late final DraggableScrollableController sheetController; + + @override + void initState() { + super.initState(); + sheetController = DraggableScrollableController(); + } + + @override + void dispose() { + sheetController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Future addToAlbum(RemoteAlbum album) async { + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); + + if (!context.mounted) { + return; + } + + if (!result.success) { + ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + return; + } + + ImmichToast.show( + context: context, + msg: result.count == 0 + ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) + : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ); + } + + Future onKeyboardExpand() { + return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); + } + + return BaseBottomSheet( + controller: sheetController, initialChildSize: 0.25, - maxChildSize: 0.4, + maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ + actions: const [ ShareActionButton(source: ActionSource.timeline), DeleteLocalActionButton(source: ActionSource.timeline), UploadActionButton(source: ActionSource.timeline), ], + slivers: [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand), + ], ); } } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 6848a07bb8..6b914ed077 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; @@ -21,7 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -56,29 +55,28 @@ class _RemoteAlbumBottomSheetState extends ConsumerState final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId; - Future addAssetsToAlbum(RemoteAlbum album) async { - final selectedAssets = multiselect.selectedAssets; - if (selectedAssets.isEmpty) { + Future addToAlbum(RemoteAlbum album) async { + final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); + + if (!context.mounted) { return; } - final addedCount = await ref - .read(remoteAlbumProvider.notifier) - .addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList()); - - if (addedCount != selectedAssets.length) { + if (!result.success) { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}), - ); - } else { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + msg: 'scaffold_body_error_occurred'.t(context: context), + toastType: ToastType.error, ); + return; } - ref.read(multiSelectProvider.notifier).reset(); + ImmichToast.show( + context: context, + msg: result.count == 0 + ? 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}) + : 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + ); } Future onKeyboardExpand() { @@ -118,10 +116,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), ], slivers: ownsAlbum - ? [ - const AddToAlbumHeader(), - AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), - ] + ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand)] : null, ); } diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 9364fdd091..36d9678277 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -4,7 +4,7 @@ import 'package:async/async.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; @@ -189,5 +189,5 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai bool _shouldUseLocalAsset(BaseAsset asset) => asset.hasLocal && - (!asset.hasRemote || !MetadataRepository.instance.appConfig.image.preferRemote) && + (!asset.hasRemote || !SettingsRepository.instance.appConfig.image.preferRemote) && !asset.isEdited; diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 6376e07405..eba4f0a1cd 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; @@ -104,7 +104,7 @@ class LocalFullImageProvider extends CancellableImageProvider { } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly); + ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); state = state.copyWith(onlyFavorites: isFavoriteOnly); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived); + ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); state = state.copyWith(includeArchived: isIncludeArchived); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchWithPartners(bool isWithPartners) { - ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners); + ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); state = state.copyWith(withPartners: isWithPartners); EventStream.shared.emit(const MapMarkerReloadEvent()); } void setRelativeTime(int relativeDays) { - ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeDays); + ref.read(settingsProvider).write(.mapRelativeDate, relativeDays); state = state.copyWith(relativeDays: relativeDays); EventStream.shared.emit(const MapMarkerReloadEvent()); } diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index 7b88800f22..8dd87f9868 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; class TimelineArgs { diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index de52c047a2..eb7a31ac8b 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -10,7 +10,6 @@ import 'package:flutter/rendering.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; @@ -22,7 +21,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -459,7 +458,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { _restoreAssetIndex = targetAssetIndex; }); - ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, _perRow); + ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow); } }; }, diff --git a/mobile/lib/providers/album/pending_album_uploads.provider.dart b/mobile/lib/providers/album/pending_album_uploads.provider.dart index db857ba3c0..7b1188891b 100644 --- a/mobile/lib/providers/album/pending_album_uploads.provider.dart +++ b/mobile/lib/providers/album/pending_album_uploads.provider.dart @@ -67,6 +67,11 @@ class AlbumPendingUploadsNotifier extends AutoDisposeFamilyNotifier { await Future.delayed(const Duration(milliseconds: 500)); final backgroundManager = _ref.read(backgroundSyncProvider); - final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums; + final isAlbumLinkedSyncEnable = _ref.read(appConfigProvider).backup.syncAlbums; try { bool syncSuccess = false; @@ -137,7 +137,7 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _resumeBackup() async { - final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled; + final isEnableBackup = _ref.read(appConfigProvider).backup.enabled; if (isEnableBackup) { final currentUser = Store.tryGet(StoreKey.currentUser); diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index ae97909349..8a3f503553 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; @@ -11,7 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; @@ -130,7 +129,7 @@ class AuthNotifier extends StateNotifier { await _apiService.updateHeaders(); final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headerMap = _ref.read(metadataProvider).systemConfig.network.customHeaders; + final headerMap = _ref.read(appConfigProvider).network.customHeaders; final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap); await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); @@ -179,19 +178,19 @@ class AuthNotifier extends StateNotifier { } Future saveWifiName(String wifiName) async { - await _ref.read(metadataProvider).write(MetadataKey.networkPreferredWifiName, wifiName); + await _ref.read(settingsProvider).write(.networkPreferredWifiName, wifiName); } Future saveLocalEndpoint(String url) async { - await _ref.read(metadataProvider).write(MetadataKey.networkLocalEndpoint, url); + await _ref.read(settingsProvider).write(.networkLocalEndpoint, url); } String? getSavedWifiName() { - return _ref.read(metadataProvider).systemConfig.network.preferredWifiName; + return _ref.read(appConfigProvider).network.preferredWifiName; } String? getSavedLocalEndpoint() { - return _ref.read(metadataProvider).systemConfig.network.localEndpoint; + return _ref.read(appConfigProvider).network.localEndpoint; } /// Returns the current server endpoint (with /api) URL from the store diff --git a/mobile/lib/providers/cleanup.provider.dart b/mobile/lib/providers/cleanup.provider.dart index e4a3d10a15..378ceb010f 100644 --- a/mobile/lib/providers/cleanup.provider.dart +++ b/mobile/lib/providers/cleanup.provider.dart @@ -1,8 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/cleanup.service.dart'; @@ -54,21 +54,21 @@ final cleanupProvider = StateNotifierProvider((re return CleanupNotifier( ref.watch(cleanupServiceProvider), ref.watch(currentUserProvider)?.id, - ref.watch(metadataProvider), + ref.watch(settingsProvider), ); }); class CleanupNotifier extends StateNotifier { final CleanupService _cleanupService; final String? _userId; - final MetadataRepository _metadataRepository; + final SettingsRepository _settingsRepository; - CleanupNotifier(this._cleanupService, this._userId, this._metadataRepository) : super(const CleanupState()) { + CleanupNotifier(this._cleanupService, this._userId, this._settingsRepository) : super(const CleanupState()) { _loadPersistedSettings(); } void _loadPersistedSettings() { - final cleanup = _metadataRepository.appConfig.cleanup; + final cleanup = _settingsRepository.appConfig.cleanup; final keepFavorites = cleanup.keepFavorites; final keepMediaType = cleanup.keepMediaType; final keepAlbumIds = cleanup.keepAlbumIds.toSet(); @@ -87,18 +87,18 @@ class CleanupNotifier extends StateNotifier { state = state.copyWith(selectedDate: date, assetsToDelete: []); if (date != null) { final daysAgo = DateTime.now().difference(date).inDays; - _metadataRepository.write(.cleanupCutoffDaysAgo, daysAgo); + _settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo); } } void setKeepMediaType(AssetKeepType keepMediaType) { state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []); - _metadataRepository.write(.cleanupKeepMediaType, keepMediaType); + _settingsRepository.write(.cleanupKeepMediaType, keepMediaType); } void setKeepFavorites(bool keepFavorites) { state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []); - _metadataRepository.write(.cleanupKeepFavorites, keepFavorites); + _settingsRepository.write(.cleanupKeepFavorites, keepFavorites); } void toggleKeepAlbum(String albumId) { @@ -118,7 +118,7 @@ class CleanupNotifier extends StateNotifier { } void _persistExcludedAlbumIds(Set albumIds) { - _metadataRepository.write(.cleanupKeepAlbumIds, albumIds.toList()); + _settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList()); } void cleanupStaleAlbumIds(Set existingAlbumIds) { @@ -131,7 +131,7 @@ class CleanupNotifier extends StateNotifier { } void applyDefaultAlbumSelections(List<(String id, String name)> albums) { - final isInitialized = _metadataRepository.appConfig.cleanup.defaultsInitialized; + final isInitialized = _settingsRepository.appConfig.cleanup.defaultsInitialized; if (isInitialized) { return; } @@ -144,7 +144,7 @@ class CleanupNotifier extends StateNotifier { _persistExcludedAlbumIds(keepAlbumIds); } - _metadataRepository.write(.cleanupDefaultsInitialized, true); + _settingsRepository.write(.cleanupDefaultsInitialized, true); } Future scanAssets() async { diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 9f01a68f03..aa734f56b8 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -5,12 +5,15 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/services/asset.service.dart'; +import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; @@ -373,6 +376,52 @@ class ActionNotifier extends Notifier { } } + Future addToAlbum(ActionSource source, RemoteAlbum album) async { + final selected = _getAssets(source).toList(growable: false); + if (selected.isEmpty) { + return const ActionResult(count: 0, success: true); + } + + final candidates = RemoteAlbumService.categorizeCandidates(selected); + final remoteIds = candidates.remoteAssetIds; + final localAssets = candidates.localAssetsToUpload; + final albumNotifier = ref.read(remoteAlbumProvider.notifier); + + int addedRemote = 0; + if (remoteIds.isNotEmpty) { + try { + addedRemote = await albumNotifier.addAssets(album.id, remoteIds); + } catch (error, stack) { + _logger.severe('Failed to add assets to album ${album.id}', error, stack); + return ActionResult(count: 0, success: false, error: error.toString()); + } + } + + // Keep the selection available for retry if the remote add fails. Once the + // album mutation succeeds, clear timeline selection so upload overlays can render. + if (source == ActionSource.timeline) { + ref.read(multiSelectProvider.notifier).reset(); + } + + if (localAssets.isEmpty) { + return ActionResult(count: addedRemote, success: true); + } + + final uploadResult = await upload( + source, + assets: localAssets, + onAssetUploaded: (asset, remoteId) async { + await albumNotifier.linkUploadedAssetToAlbum(album.id, asset, remoteId); + }, + ); + + return ActionResult( + count: addedRemote + uploadResult.count, + success: uploadResult.success, + error: uploadResult.error, + ); + } + Future removeFromAlbum(ActionSource source, String albumId) async { final ids = _getRemoteIdsForSource(source); try { @@ -465,11 +514,17 @@ class ActionNotifier extends Notifier { ActionSource source, BuildContext context, { Completer? cancelCompleter, + void Function(double progress)? onAssetDownloadProgress, }) async { final ids = _getAssets(source).toList(growable: false); try { - await _service.shareAssets(ids, context, cancelCompleter: cancelCompleter); + await _service.shareAssets( + ids, + context, + cancelCompleter: cancelCompleter, + onAssetDownloadProgress: onAssetDownloadProgress, + ); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to share assets', error, stack); @@ -489,8 +544,16 @@ class ActionNotifier extends Notifier { } } - Future upload(ActionSource source, {List? assets}) async { + Future upload( + ActionSource source, { + List? assets, + FutureOr Function(LocalAsset asset, String remoteId)? onAssetUploaded, + }) async { final assetsToUpload = assets ?? _getAssets(source).whereType().toList(); + final assetById = {for (final a in assetsToUpload) a.id: a}; + final uploadedAssetIds = {}; + final failedAssetIds = {}; + final postUploadTasks = >[]; final progressNotifier = ref.read(assetUploadProgressProvider.notifier); final cancelToken = Completer(); @@ -512,16 +575,43 @@ class ActionNotifier extends Notifier { }, onSuccess: (localAssetId, remoteAssetId) { progressNotifier.remove(localAssetId); + uploadedAssetIds.add(localAssetId); + final asset = assetById[localAssetId]; + final callback = onAssetUploaded; + if (asset != null && callback != null) { + postUploadTasks.add( + Future.sync(() => callback(asset, remoteAssetId)).catchError((Object error, StackTrace stack) { + failedAssetIds.add(localAssetId); + progressNotifier.setError(localAssetId); + _logger.warning('Post-upload callback failed for $localAssetId', error, stack); + }), + ); + } }, onError: (localAssetId, errorMessage) { + failedAssetIds.add(localAssetId); progressNotifier.setError(localAssetId); }, ), ); - return ActionResult(count: assetsToUpload.length, success: true); + + await Future.wait(postUploadTasks); + final successCount = uploadedAssetIds.difference(failedAssetIds).length; + final isSuccess = successCount == assetsToUpload.length && failedAssetIds.isEmpty; + + return ActionResult( + count: successCount, + success: isSuccess, + error: isSuccess ? null : 'Failed to upload ${assetsToUpload.length - successCount} assets', + ); } catch (error, stack) { _logger.severe('Failed manually upload assets', error, stack); - return ActionResult(count: assetsToUpload.length, success: false, error: error.toString()); + + return ActionResult( + count: uploadedAssetIds.difference(failedAssetIds).length, + success: false, + error: error.toString(), + ); } finally { ref.read(manualUploadCancelTokenProvider.notifier).state = null; Future.delayed(const Duration(seconds: 2), () { diff --git a/mobile/lib/providers/infrastructure/metadata.provider.dart b/mobile/lib/providers/infrastructure/metadata.provider.dart deleted file mode 100644 index 46ff1069f9..0000000000 --- a/mobile/lib/providers/infrastructure/metadata.provider.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/domain/models/config/system_config.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; - -final metadataProvider = Provider.autoDispose((_) => MetadataRepository.instance); - -final appConfigProvider = Provider.autoDispose((ref) { - final repo = ref.watch(metadataProvider); - final subscription = repo.watchAppConfig().listen((event) => ref.state = event); - ref.onDispose(subscription.cancel); - return repo.appConfig; -}); - -final systemConfigProvider = Provider.autoDispose((ref) { - final repo = ref.watch(metadataProvider); - final subscription = repo.watchSystemConfig().listen((event) => ref.state = event); - ref.onDispose(subscription.cancel); - return repo.systemConfig; -}); diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 73a796bd31..a4bbbae818 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; @@ -207,6 +208,22 @@ class RemoteAlbumNotifier extends Notifier { return added; } + /// Links a freshly-uploaded local asset to an album using its new remote ID, + /// upserting a placeholder remote asset row so the local DB join survives + /// until the next sync catches up. + Future linkUploadedAssetToAlbum(String albumId, LocalAsset source, String remoteId) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + throw Exception('User not logged in'); + } + + final added = await _remoteAlbumService.linkUploadedAssetToAlbum(albumId, remoteId, currentUser, source); + if (added > 0) { + await _refreshAlbumInState(albumId); + } + return added; + } + /// Adds a heterogeneous asset selection to an album. Already-remote assets /// are linked immediately; local-only assets are queued in /// [pendingAlbumUploadsProvider] (so the album page can show them with @@ -221,11 +238,18 @@ class RemoteAlbumNotifier extends Notifier { final pendingNotifier = ref.read(pendingAlbumUploadsProvider(albumId).notifier); pendingNotifier.enqueue(candidates.localAssetsToUpload); + Completer? cancelToken; + if (candidates.localAssetsToUpload.isNotEmpty) { + cancelToken = Completer(); + ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken; + } + try { final added = await _remoteAlbumService.addAssetsToAlbum( albumId: albumId, uploader: currentUser, candidates: candidates, + cancelToken: cancelToken, uploadCallbacks: UploadCallbacks( onProgress: (localAssetId, _, bytes, totalBytes) { final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; @@ -245,6 +269,15 @@ class RemoteAlbumNotifier extends Notifier { } _logger.severe('Failed to add assets to album $albumId', error, stack); rethrow; + } finally { + if (cancelToken != null) { + if (cancelToken.isCompleted) { + pendingNotifier.clear(); + } + if (ref.read(manualUploadCancelTokenProvider) == cancelToken) { + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + } + } } } diff --git a/mobile/lib/providers/infrastructure/settings.provider.dart b/mobile/lib/providers/infrastructure/settings.provider.dart new file mode 100644 index 0000000000..d2b9dce1d6 --- /dev/null +++ b/mobile/lib/providers/infrastructure/settings.provider.dart @@ -0,0 +1,12 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; + +final settingsProvider = Provider.autoDispose((_) => SettingsRepository.instance); + +final appConfigProvider = Provider.autoDispose((ref) { + final repo = ref.watch(settingsProvider); + final subscription = repo.watchConfig().listen((event) => ref.state = event); + ref.onDispose(subscription.cancel); + return repo.appConfig; +}); diff --git a/mobile/lib/providers/infrastructure/timeline.provider.dart b/mobile/lib/providers/infrastructure/timeline.provider.dart index 9f2fdec519..b22c693033 100644 --- a/mobile/lib/providers/infrastructure/timeline.provider.dart +++ b/mobile/lib/providers/infrastructure/timeline.provider.dart @@ -3,7 +3,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; final timelineRepositoryProvider = Provider( @@ -29,7 +29,7 @@ final timelineServiceProvider = Provider( final timelineFactoryProvider = Provider( (ref) => TimelineFactory( timelineRepository: ref.watch(timelineRepositoryProvider), - metadataRepository: ref.watch(metadataProvider), + settingsRepository: ref.watch(settingsProvider), ), ); diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index b0a59f6a1e..b643264dca 100644 --- a/mobile/lib/providers/map/map_state.provider.dart +++ b/mobile/lib/providers/map/map_state.provider.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; final mapStateNotifierProvider = NotifierProvider(MapStateNotifier.new); @@ -27,12 +26,12 @@ class MapStateNotifier extends Notifier { } void switchTheme(ThemeMode mode) { - ref.read(metadataProvider).write(MetadataKey.mapThemeMode, mode); + ref.read(settingsProvider).write(.mapThemeMode, mode); state = state.copyWith(themeMode: mode); } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly); + ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true); } @@ -41,17 +40,17 @@ class MapStateNotifier extends Notifier { } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived); + ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true); } void switchWithPartners(bool isWithPartners) { - ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners); + ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true); } void setRelativeTime(int relativeTime) { - ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeTime); + ref.read(settingsProvider).write(.mapRelativeDate, relativeTime); state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true); } } diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart index 909b8137c1..962781586e 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index a53f0aaaeb..8d9bd5bfe3 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -193,7 +193,7 @@ class WebsocketNotifier extends StateNotifier { return; } - final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; + final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums; try { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { @@ -214,7 +214,7 @@ class WebsocketNotifier extends StateNotifier { return; } - final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; + final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums; try { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) { diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 6b34d1855f..e86a372768 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -1,31 +1,29 @@ import 'dart:async'; import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:share_plus/share_plus.dart'; -final assetMediaRepositoryProvider = Provider( - (ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider), ref.watch(nativeSyncApiProvider)), -); +final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(nativeSyncApiProvider))); class AssetMediaRepository { - final AssetApiRepository _assetApiRepository; final NativeSyncApi _nativeSyncApi; static final Logger _log = Logger("AssetMediaRepository"); - const AssetMediaRepository(this._assetApiRepository, this._nativeSyncApi); + const AssetMediaRepository(this._nativeSyncApi); Future _androidSupportsTrash() async { if (Platform.isAndroid) { @@ -107,10 +105,29 @@ class AssetMediaRepository { ); } - // TODO: make this more efficient - Future shareAssets(List assets, BuildContext context, {Completer? cancelCompleter}) async { + Future shareAssets( + List assets, + BuildContext context, { + Completer? cancelCompleter, + void Function(double progress)? onAssetDownloadProgress, + }) async { final downloadedXFiles = []; final tempFiles = []; + final totalAssets = assets.length; + var processedAssets = 0; + + void updateProgress([double currentAssetProgress = 0.0]) { + if (totalAssets <= 0) { + onAssetDownloadProgress?.call(1.0); + return; + } + + final normalizedAssetProgress = currentAssetProgress.clamp(0.0, 1.0); + final overallProgress = ((processedAssets + normalizedAssetProgress) / totalAssets).clamp(0.0, 1.0); + onAssetDownloadProgress?.call(overallProgress); + } + + updateProgress(); for (var asset in assets) { if (cancelCompleter != null && cancelCompleter.isCompleted) { @@ -127,6 +144,8 @@ class AssetMediaRepository { if (localId != null && !asset.isEdited) { File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile; downloadedXFiles.add(XFile(f!.path)); + processedAssets++; + updateProgress(); if (CurrentPlatform.isIOS) { tempFiles.add(f); } @@ -134,22 +153,50 @@ class AssetMediaRepository { final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId; if (remoteId == null) { _log.warning("Asset has no remote ID for sharing: $asset"); + processedAssets++; + updateProgress(); continue; } - final tempDir = await getTemporaryDirectory(); - final name = asset.name; - final tempFile = await File('${tempDir.path}/$name').create(); - final res = await _assetApiRepository.downloadAsset(remoteId, edited: true); + final taskId = 'share-$remoteId-${DateTime.now().microsecondsSinceEpoch}'; + final sanitizedFilename = asset.name.replaceAll(RegExp(r'[\\/]'), '_'); + final task = DownloadTask( + taskId: taskId, + url: getOriginalUrlForRemoteId(remoteId, edited: asset.isEdited), + headers: ApiService.getRequestHeaders(), + filename: sanitizedFilename, + baseDirectory: BaseDirectory.temporary, + group: kShareDownloadGroup, + updates: Updates.statusAndProgress, + ); + final statusUpdate = await FileDownloader().download( + task, + onProgress: (value) { + if (cancelCompleter != null && cancelCompleter.isCompleted) { + unawaited(FileDownloader().cancelTaskWithId(taskId)); + return; + } + updateProgress(value); + }, + ); - if (res.statusCode != 200) { - _log.severe("Download for $name failed", res.toLoggerString()); - continue; + if (cancelCompleter != null && cancelCompleter.isCompleted) { + await _cleanupTempFiles(tempFiles); + return 0; } - await tempFile.writeAsBytes(res.bodyBytes); - downloadedXFiles.add(XFile(tempFile.path)); - tempFiles.add(tempFile); + if (statusUpdate.status == TaskStatus.complete) { + final filePath = await task.filePath(); + final file = File(filePath); + tempFiles.add(file); + downloadedXFiles.add(XFile(filePath)); + processedAssets++; + updateProgress(); + continue; + } + _log.severe("Download for ${asset.name} failed with status ${statusUpdate.status}", statusUpdate.exception); + processedAssets++; + updateProgress(); } } diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index e71c752f44..dd9aed3a03 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,39 +1,39 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(metadataProvider)), + (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(settingsProvider)), ); class AuthRepository { final Drift _drift; - final MetadataRepository _metadata; + final SettingsRepository _settings; - const AuthRepository(this._drift, this._metadata); + const AuthRepository(this._drift, this._settings); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); } bool getEndpointSwitchingFeature() { - return _metadata.systemConfig.network.autoEndpointSwitching; + return _settings.appConfig.network.autoEndpointSwitching; } String? getPreferredWifiName() { - return _metadata.systemConfig.network.preferredWifiName; + return _settings.appConfig.network.preferredWifiName; } String? getLocalEndpoint() { - return _metadata.systemConfig.network.localEndpoint; + return _settings.appConfig.network.localEndpoint; } List getExternalEndpointList() { - return _metadata.systemConfig.network.externalEndpointList + return _settings.appConfig.network.externalEndpointList .map((url) => AuxilaryEndpoint(url: url, status: .valid)) .toList(); } diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index b9e601ca25..b22c6680a4 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -269,8 +269,18 @@ class ActionService { await _assetApiRepository.unStack(stackIds); } - Future shareAssets(List assets, BuildContext context, {Completer? cancelCompleter}) { - return _assetMediaRepository.shareAssets(assets, context, cancelCompleter: cancelCompleter); + Future shareAssets( + List assets, + BuildContext context, { + Completer? cancelCompleter, + void Function(double progress)? onAssetDownloadProgress, + }) { + return _assetMediaRepository.shareAssets( + assets, + context, + cancelCompleter: cancelCompleter, + onAssetDownloadProgress: onAssetDownloadProgress, + ); } Future> downloadAll(List assets) { diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index bc36c98768..a0828927ce 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/url_helper.dart'; @@ -13,7 +13,7 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; class ApiService { - late ApiClient _apiClient; + final ApiClient _apiClient = ApiClient(basePath: ''); late UsersApi usersApi; late AuthenticationApi authenticationApi; @@ -54,7 +54,7 @@ class ApiService { } setEndpoint(String endpoint) { - _apiClient = ApiClient(basePath: endpoint); + _apiClient.basePath = endpoint; _apiClient.client = NetworkRepository.client; usersApi = UsersApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient); @@ -177,9 +177,9 @@ class ApiService { if (serverEndpoint != null && serverEndpoint.isNotEmpty) { urls.add(serverEndpoint); } - final network = MetadataRepository.instance.systemConfig.network; + final network = SettingsRepository.instance.appConfig.network; final localEndpoint = network.localEndpoint; - if (localEndpoint != null) { + if (localEndpoint.isNotEmpty) { urls.add(localEndpoint); } for (final url in network.externalEndpointList) { @@ -191,7 +191,7 @@ class ApiService { } static Map getRequestHeaders() { - return MetadataRepository.instance.systemConfig.network.customHeaders; + return SettingsRepository.instance.appConfig.network.customHeaders; } ApiClient get apiClient => _apiClient; diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 7d470ecd7a..0de22fd124 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; @@ -100,7 +100,7 @@ class AuthService { _log.severe("Error clearing local data", error, stackTrace); }); - await MetadataRepository.instance.write(MetadataKey.backupEnabled, false); + await SettingsRepository.instance.write(SettingsKey.backupEnabled, false); } } @@ -110,7 +110,7 @@ class AuthService { /// - Authentication repository data /// - Current user information /// - Access token - /// - Asset ETag + /// - Server-specific endpoint configuration /// /// All deletions are executed in parallel using [Future.wait]. Future clearLocalData() async { @@ -120,6 +120,12 @@ class AuthService { _authRepository.clearLocalData(), Store.delete(StoreKey.currentUser), Store.delete(StoreKey.accessToken), + SettingsRepository.instance.clear(const [ + .networkAutoEndpointSwitching, + .networkPreferredWifiName, + .networkLocalEndpoint, + .networkExternalEndpointList, + ]), ]); } diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 37577e3666..903fd02395 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -13,7 +13,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; @@ -359,7 +359,7 @@ class BackgroundUploadService { } bool _shouldRequireWiFi(LocalAsset asset) { - final backup = MetadataRepository.instance.appConfig.backup; + final backup = SettingsRepository.instance.appConfig.backup; if (asset.isVideo && backup.useCellularForVideos) { return false; } diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 2fc1a92127..ef7f32d168 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; @@ -451,7 +451,7 @@ class ForegroundUploadService { } bool _shouldRequireWiFi(LocalAsset asset) { - final backup = MetadataRepository.instance.appConfig.backup; + final backup = SettingsRepository.instance.appConfig.backup; if (asset.isVideo && backup.useCellularForVideos) { return false; } diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 68ebfe9c9f..9bd652381a 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -49,11 +49,11 @@ abstract final class Bootstrap { await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); - final metadataRepo = await MetadataRepository.ensureInitialized(drift); + final settingsRepo = await SettingsRepository.ensureInitialized(drift); await LogService.init( logRepository: LogRepository(logDb), - metadataRepository: metadataRepo, + settingsRepository: settingsRepo, shouldBuffer: shouldBufferLogs, ); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 41301fb227..3afa554e29 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,16 +1,18 @@ import 'dart:async'; import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; @@ -72,80 +74,76 @@ Future _migrateTo25() async { Future _migrateTo26(Drift drift) async { final migrator = _StoreMigrator(drift); - await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values); + await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, SettingsKey.logLevel, LogLevel.values); // Theme - await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values); - await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.themePrimaryColor, ImmichColorPreset.values); - await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.themeDynamic); - await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.themeColorfulInterface); + await migrator.migrateEnumName(StoreKey.legacyThemeMode, SettingsKey.themeMode, ThemeMode.values); + await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, SettingsKey.themePrimaryColor, ImmichColorPreset.values); + await migrator.migrateBool(StoreKey.legacyDynamicTheme, SettingsKey.themeDynamic); + await migrator.migrateBool(StoreKey.legacyColorfulInterface, SettingsKey.themeColorfulInterface); // Cleanup final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id); if (cleanupKeepAlbumIds != null) { final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); - migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, MetadataKey.cleanupKeepAlbumIds, ids); + migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, SettingsKey.cleanupKeepAlbumIds, ids); } - await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites); + await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, SettingsKey.cleanupKeepFavorites); await migrator.migrateEnumIndex( StoreKey.legacyCleanupKeepMediaType, - MetadataKey.cleanupKeepMediaType, + SettingsKey.cleanupKeepMediaType, AssetKeepType.values, ); - await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, MetadataKey.cleanupCutoffDaysAgo); - await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, MetadataKey.cleanupDefaultsInitialized); + await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, SettingsKey.cleanupCutoffDaysAgo); + await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, SettingsKey.cleanupDefaultsInitialized); // Map - await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, MetadataKey.mapShowFavoriteOnly); - await migrator.migrateInt(StoreKey.legacyMapRelativeDate, MetadataKey.mapRelativeDate); - await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, MetadataKey.mapIncludeArchived); - await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, MetadataKey.mapThemeMode, ThemeMode.values); - await migrator.migrateBool(StoreKey.legacyMapwithPartners, MetadataKey.mapWithPartners); + await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, SettingsKey.mapShowFavoriteOnly); + await migrator.migrateInt(StoreKey.legacyMapRelativeDate, SettingsKey.mapRelativeDate); + await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, SettingsKey.mapIncludeArchived); + await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, SettingsKey.mapThemeMode, ThemeMode.values); + await migrator.migrateBool(StoreKey.legacyMapwithPartners, SettingsKey.mapWithPartners); // Timeline - await migrator.migrateInt(StoreKey.legacyTilesPerRow, MetadataKey.timelineTilesPerRow); + await migrator.migrateInt(StoreKey.legacyTilesPerRow, SettingsKey.timelineTilesPerRow); await migrator.migrateEnumIndex( StoreKey.legacyGroupAssetsBy, - MetadataKey.timelineGroupAssetsBy, + SettingsKey.timelineGroupAssetsBy, GroupAssetsBy.values, ); - await migrator.migrateBool(StoreKey.legacyStorageIndicator, MetadataKey.timelineStorageIndicator); + await migrator.migrateBool(StoreKey.legacyStorageIndicator, SettingsKey.timelineStorageIndicator); // Image - await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, MetadataKey.imagePreferRemote); - await migrator.migrateBool(StoreKey.legacyLoadOriginal, MetadataKey.imageLoadOriginal); + await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, SettingsKey.imagePreferRemote); + await migrator.migrateBool(StoreKey.legacyLoadOriginal, SettingsKey.imageLoadOriginal); // Viewer - await migrator.migrateBool(StoreKey.legacyLoopVideo, MetadataKey.viewerLoopVideo); - await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, MetadataKey.viewerLoadOriginalVideo); - await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, MetadataKey.viewerAutoPlayVideo); - await migrator.migrateBool(StoreKey.legacyTapToNavigate, MetadataKey.viewerTapToNavigate); + await migrator.migrateBool(StoreKey.legacyLoopVideo, SettingsKey.viewerLoopVideo); + await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, SettingsKey.viewerLoadOriginalVideo); + await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, SettingsKey.viewerAutoPlayVideo); + await migrator.migrateBool(StoreKey.legacyTapToNavigate, SettingsKey.viewerTapToNavigate); // Network - await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, MetadataKey.networkAutoEndpointSwitching); - await migrator.migrateString(StoreKey.legacyPreferredWifiName, MetadataKey.networkPreferredWifiName); - await migrator.migrateString(StoreKey.legacyLocalEndpoint, MetadataKey.networkLocalEndpoint); + await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, SettingsKey.networkAutoEndpointSwitching); + await migrator.migrateString(StoreKey.legacyPreferredWifiName, SettingsKey.networkPreferredWifiName); + await migrator.migrateString(StoreKey.legacyLocalEndpoint, SettingsKey.networkLocalEndpoint); await _migrateExternalEndpointList(migrator); await _migrateCustomHeaders(migrator); // Album await _migrateAlbumSortMode(migrator); - await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse); - await migrator.migrateBool(StoreKey.legacyAlbumGridView, MetadataKey.albumIsGrid); + await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, SettingsKey.albumIsReverse); + await migrator.migrateBool(StoreKey.legacyAlbumGridView, SettingsKey.albumIsGrid); // Backup - await migrator.migrateBool(StoreKey.legacyEnableBackup, MetadataKey.backupEnabled); - await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, MetadataKey.backupUseCellularForVideos); - await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, MetadataKey.backupUseCellularForPhotos); - await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, MetadataKey.backupRequireCharging); - await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, MetadataKey.backupTriggerDelay); - await migrator.migrateBool(StoreKey.legacySyncAlbums, MetadataKey.backupSyncAlbums); + await migrator.migrateBool(StoreKey.legacyEnableBackup, SettingsKey.backupEnabled); + await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, SettingsKey.backupUseCellularForVideos); + await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, SettingsKey.backupUseCellularForPhotos); + await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, SettingsKey.backupRequireCharging); + await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, SettingsKey.backupTriggerDelay); + await migrator.migrateBool(StoreKey.legacySyncAlbums, SettingsKey.backupSyncAlbums); await migrator.complete(); } Future _migrateAlbumSortMode(_StoreMigrator migrator) async { final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id); - if (raw == null) { + final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw); + if (mode == null) { return; } - final mode = AlbumSortMode.values.firstWhere( - (e) => e.storeIndex == raw, - orElse: () => MetadataKey.albumSortMode.defaultValue, - ); - - migrator.stage(StoreKey.legacySelectedAlbumSortOrder, MetadataKey.albumSortMode, mode); + migrator.stage(StoreKey.legacySelectedAlbumSortOrder, SettingsKey.albumSortMode, mode); } Future _migrateExternalEndpointList(_StoreMigrator migrator) async { @@ -169,7 +167,7 @@ Future _migrateExternalEndpointList(_StoreMigrator migrator) async { // ignore invalid entries } - migrator.stage(StoreKey.legacyExternalEndpointList, MetadataKey.networkExternalEndpointList, urls); + migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls); } Future _migrateCustomHeaders(_StoreMigrator migrator) async { @@ -192,30 +190,34 @@ Future _migrateCustomHeaders(_StoreMigrator migrator) async { // ignore invalid entries } - migrator.stage(StoreKey.legacyCustomHeaders, MetadataKey.networkCustomHeaders, headers); + migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers); } class _StoreMigrator { final Drift _db; - final Map, Object> _cache = {}; + final Map, Object> _cache = {}; final List _migratedStoreIds = []; _StoreMigrator(this._db); - Future migrateEnumIndex(StoreKey legacyKey, MetadataKey newKey, List values) async { + Future migrateEnumIndex(StoreKey legacyKey, SettingsKey newKey, List values) async { final index = await readLegacyStoreInt(legacyKey.id); if (index == null) { return; } - final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue; + final enumValue = values.elementAtOrNull(index); + if (enumValue == null) { + return; + } + _cache[newKey] = enumValue; _migratedStoreIds.add(legacyKey.id); } Future migrateEnumName( StoreKey legacyKey, - MetadataKey newKey, + SettingsKey newKey, List values, ) async { final name = await readLegacyStoreString(legacyKey.id); @@ -223,12 +225,16 @@ class _StoreMigrator { return; } - final enumValue = values.firstWhere((e) => e.name == name, orElse: () => newKey.defaultValue); + final enumValue = values.firstWhereOrNull((e) => e.name == name); + if (enumValue == null) { + return; + } + _cache[newKey] = enumValue; _migratedStoreIds.add(legacyKey.id); } - Future migrateBool(StoreKey legacyKey, MetadataKey newKey) async { + Future migrateBool(StoreKey legacyKey, SettingsKey newKey) async { final intValue = await readLegacyStoreInt(legacyKey.id); if (intValue == null) { return; @@ -239,7 +245,7 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateInt(StoreKey legacyKey, MetadataKey newKey) async { + Future migrateInt(StoreKey legacyKey, SettingsKey newKey) async { final intValue = await readLegacyStoreInt(legacyKey.id); if (intValue == null) { return; @@ -249,7 +255,7 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateString(StoreKey legacyKey, MetadataKey newKey) async { + Future migrateString(StoreKey legacyKey, SettingsKey newKey) async { final value = await readLegacyStoreString(legacyKey.id); if (value == null) { return; @@ -259,7 +265,7 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - void stage(StoreKey legacyKey, MetadataKey newKey, T value) { + void stage(StoreKey legacyKey, SettingsKey newKey, T value) { _cache[newKey] = value; _migratedStoreIds.add(legacyKey.id); } @@ -267,9 +273,12 @@ class _StoreMigrator { Future complete() async { await _db.batch((batch) { for (final entry in _cache.entries) { + if (entry.value == defaultConfig.read(entry.key)) { + continue; + } batch.insert( - _db.metadataEntity, - MetadataEntityCompanion(key: Value(entry.key.key), value: Value(entry.key.encode(entry.value))), + _db.settingsEntity, + SettingsEntityCompanion(key: Value(entry.key.name), value: Value(entry.key.encode(entry.value))), mode: InsertMode.insertOrReplace, ); } diff --git a/mobile/lib/utils/semver.dart b/mobile/lib/utils/semver.dart index 06b186daa3..8080af00b0 100644 --- a/mobile/lib/utils/semver.dart +++ b/mobile/lib/utils/semver.dart @@ -1,36 +1,42 @@ -enum SemVerType { major, minor, patch } +enum SemVerType { major, minor, patch, prerelease } class SemVer { final int major; final int minor; final int patch; + final int? prerelease; - const SemVer({required this.major, required this.minor, required this.patch}); + const SemVer({required this.major, required this.minor, required this.patch, this.prerelease}); @override String toString() { - return '$major.$minor.$patch'; + return '$major.$minor.$patch${prerelease == null ? '' : '-rc.$prerelease'}'; } - SemVer copyWith({int? major, int? minor, int? patch}) { - return SemVer(major: major ?? this.major, minor: minor ?? this.minor, patch: patch ?? this.patch); + SemVer copyWith({int? major, int? minor, int? patch, int? prerelease}) { + return SemVer( + major: major ?? this.major, + minor: minor ?? this.minor, + patch: patch ?? this.patch, + prerelease: prerelease ?? this.prerelease, + ); } + static final _pattern = RegExp(r'^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?(?:[-+].*)?$', caseSensitive: false); + factory SemVer.fromString(String version) { - if (version.toLowerCase().startsWith("v")) { - version = version.substring(1); - } - - final parts = version.split("-")[0].split('.'); - if (parts.length != 3) { + final match = _pattern.firstMatch(version); + if (match == null) { throw FormatException('Invalid semantic version string: $version'); } - try { - return SemVer(major: int.parse(parts[0]), minor: int.parse(parts[1]), patch: int.parse(parts[2])); - } catch (e) { - throw FormatException('Invalid semantic version string: $version'); - } + final prerelease = match.group(4); + return SemVer( + major: int.parse(match.group(1)!), + minor: int.parse(match.group(2)!), + patch: int.parse(match.group(3)!), + prerelease: prerelease == null ? null : int.parse(prerelease), + ); } bool operator >(SemVer other) { @@ -40,7 +46,10 @@ class SemVer { if (minor != other.minor) { return minor > other.minor; } - return patch > other.patch; + if (patch != other.patch) { + return patch > other.patch; + } + return _comparePrerelease(other) > 0; } bool operator <(SemVer other) { @@ -50,7 +59,23 @@ class SemVer { if (minor != other.minor) { return minor < other.minor; } - return patch < other.patch; + if (patch != other.patch) { + return patch < other.patch; + } + return _comparePrerelease(other) < 0; + } + + int _comparePrerelease(SemVer other) { + if (prerelease == other.prerelease) { + return 0; + } + if (prerelease == null) { + return 1; + } + if (other.prerelease == null) { + return -1; + } + return prerelease!.compareTo(other.prerelease!); } bool operator >=(SemVer other) { @@ -67,7 +92,11 @@ class SemVer { return true; } - return other is SemVer && other.major == major && other.minor == minor && other.patch == patch; + return other is SemVer && + other.major == major && + other.minor == minor && + other.patch == patch && + other.prerelease == prerelease; } SemVerType? differenceType(SemVer other) { @@ -80,10 +109,13 @@ class SemVer { if (patch != other.patch) { return SemVerType.patch; } + if (prerelease != other.prerelease) { + return SemVerType.prerelease; + } return null; } @override - int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode; + int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode ^ prerelease.hashCode; } 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 2809505c58..a209d280c3 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 @@ -50,9 +50,7 @@ class AppBarServerInfo extends HookConsumerWidget { divider, _ServerInfoItem( label: "server_version".tr(), - text: serverInfoState.serverVersion.major > 0 - ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}" - : "--", + text: serverInfoState.serverVersion.major > 0 ? "${serverInfoState.serverVersion}" : "--", ), divider, _ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true), @@ -60,9 +58,7 @@ class AppBarServerInfo extends HookConsumerWidget { divider, _ServerInfoItem( label: "latest_version".tr(), - text: serverInfoState.latestVersion!.major > 0 - ? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}" - : "--", + text: serverInfoState.latestVersion!.major > 0 ? "${serverInfoState.latestVersion!}" : "--", tooltip: true, icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate ? const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12) diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 32aa766dec..a81082753a 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index f64e7cc197..090c9bb2b8 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -15,7 +15,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; @@ -187,7 +187,7 @@ class LoginForm extends HookConsumerWidget { await backgroundManager.syncRemote(); await backgroundManager.hashAssets(); - if (MetadataRepository.instance.appConfig.backup.syncAlbums) { + if (SettingsRepository.instance.appConfig.backup.syncAlbums) { await backgroundManager.syncLinkedAlbum(); } } diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 5de2570737..542a7cc5e2 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -7,7 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; @@ -31,11 +31,11 @@ class AdvancedSettings extends HookConsumerWidget { final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); final isManageMediaSupported = useState(false); final manageMediaAndroidPermission = useState(false); - final levelId = useState(ref.read(systemConfigProvider).logLevel.index); + final levelId = useState(ref.read(appConfigProvider).logLevel.index); final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote); useValueChanged( preferRemote.value, - (_, __) => ref.read(metadataProvider).write(.imagePreferRemote, preferRemote.value), + (_, __) => ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value), ); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); 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 b9f81da79e..59adb335bb 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 @@ -3,11 +3,10 @@ import 'dart:async'; 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/metadata_key.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; @@ -19,7 +18,7 @@ class GroupSettings extends HookConsumerWidget { final groupBy = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.groupAssetsBy))); Future updateAppSettings(GroupAssetsBy groupBy) async { - await ref.read(metadataProvider).write(MetadataKey.timelineGroupAssetsBy, groupBy); + await ref.read(settingsProvider).write(.timelineGroupAssetsBy, groupBy); ref.invalidate(appSettingsServiceProvider); } diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 20025286f4..f915df04f8 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -2,10 +2,9 @@ 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/metadata_key.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; @@ -16,7 +15,7 @@ class LayoutSettings extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tilesPerRow = useState(ref.read(appConfigProvider.select((s) => s.timeline.tilesPerRow))); useValueChanged(tilesPerRow.value, (_, __) { - ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, tilesPerRow.value); + ref.read(settingsProvider).write(.timelineTilesPerRow, tilesPerRow.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index 21d751c26f..3ac72d6612 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -2,10 +2,8 @@ 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/metadata_key.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_layout_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -23,7 +21,7 @@ class AssetListSettings extends HookConsumerWidget { valueNotifier: storageIndicator, title: 'theme_setting_asset_list_storage_indicator_title'.tr(), onChanged: (value) { - ref.read(metadataProvider).write(MetadataKey.timelineStorageIndicator, value); + ref.read(settingsProvider).write(.timelineStorageIndicator, value); ref.invalidate(appSettingsServiceProvider); ref.invalidate(settingsProvider); }, diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index 7858033401..f65af6af9d 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -14,7 +14,7 @@ class ImageViewerQualitySetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal); useValueChanged(isOriginal.value, (_, __) { - ref.read(metadataProvider).write(.imageLoadOriginal, isOriginal.value); + ref.read(settingsProvider).write(.imageLoadOriginal, isOriginal.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart index 5af64b0be9..730521e3c1 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -13,7 +13,7 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tapToNavigate = useState(ref.read(appConfigProvider).viewer.tapToNavigate); useValueChanged(tapToNavigate.value, (_, __) { - ref.read(metadataProvider).write(.viewerTapToNavigate, tapToNavigate.value); + ref.read(settingsProvider).write(.viewerTapToNavigate, tapToNavigate.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart index 4e566e6065..5f93d429b0 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; @@ -23,19 +23,19 @@ class SlideshowSettings extends HookConsumerWidget { final useDirection = useState(slideshow.direction); useValueChanged(useTransition.value, (_, __) { - ref.read(metadataProvider).write(.slideshowTransition, useTransition.value); + ref.read(settingsProvider).write(.slideshowTransition, useTransition.value); }); useValueChanged(useRepeat.value, (_, __) { - ref.read(metadataProvider).write(.slideshowRepeat, useRepeat.value); + ref.read(settingsProvider).write(.slideshowRepeat, useRepeat.value); }); useValueChanged(useDuration.value, (_, __) { - ref.read(metadataProvider).write(.slideshowDuration, useDuration.value); + ref.read(settingsProvider).write(.slideshowDuration, useDuration.value); }); useValueChanged(useLook.value, (_, __) { - ref.read(metadataProvider).write(.slideshowLook, useLook.value); + ref.read(settingsProvider).write(.slideshowLook, useLook.value); }); useValueChanged(useDirection.value, (_, __) { - ref.read(metadataProvider).write(.slideshowDirection, useDirection.value); + ref.read(settingsProvider).write(.slideshowDirection, useDirection.value); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 8d62544dd4..81929d95b9 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -17,13 +17,13 @@ class VideoViewerSettings extends HookConsumerWidget { final useOriginalVideo = useState(viewer.loadOriginalVideo); useValueChanged(useAutoPlayVideo.value, (_, __) { - ref.read(metadataProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value); + ref.read(settingsProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value); }); useValueChanged(useLoopVideo.value, (_, __) { - ref.read(metadataProvider).write(.viewerLoopVideo, useLoopVideo.value); + ref.read(settingsProvider).write(.viewerLoopVideo, useLoopVideo.value); }); useValueChanged(useOriginalVideo.value, (_, __) { - ref.read(metadataProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value); + ref.read(settingsProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value); }); return Column( diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index 89d6f13f43..e5debb43fe 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -5,15 +5,15 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; @@ -112,7 +112,7 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> trailing: Switch( value: albumSyncEnable, onChanged: (bool newValue) async { - await ref.read(metadataProvider).write(MetadataKey.backupSyncAlbums, newValue); + await ref.read(settingsProvider).write(.backupSyncAlbums, newValue); if (newValue == true) { await _manageLinkedAlbums(); @@ -158,7 +158,7 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> } class _BackupSwitchTile extends ConsumerWidget { - final MetadataKey metadataKey; + final SettingsKey metadataKey; final bool Function(AppConfig) selector; final String titleKey; final String subtitleKey; @@ -183,7 +183,7 @@ class _BackupSwitchTile extends ConsumerWidget { trailing: Switch( value: value, onChanged: (bool newValue) async { - await ref.read(metadataProvider).write(metadataKey, newValue); + await ref.read(settingsProvider).write(metadataKey, newValue); onChanged?.call(newValue); }, ), @@ -198,7 +198,7 @@ class _UseCellularForVideosButton extends StatelessWidget { @override Widget build(BuildContext context) { return _BackupSwitchTile( - metadataKey: MetadataKey.backupUseCellularForVideos, + metadataKey: SettingsKey.backupUseCellularForVideos, selector: (c) => c.backup.useCellularForVideos, titleKey: "videos", subtitleKey: "network_requirement_videos_upload", @@ -212,7 +212,7 @@ class _UseCellularForPhotosButton extends StatelessWidget { @override Widget build(BuildContext context) { return _BackupSwitchTile( - metadataKey: MetadataKey.backupUseCellularForPhotos, + metadataKey: SettingsKey.backupUseCellularForPhotos, selector: (c) => c.backup.useCellularForPhotos, titleKey: "photos", subtitleKey: "network_requirement_photos_upload", @@ -227,7 +227,7 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final fgService = ref.read(backgroundWorkerFgServiceProvider); return _BackupSwitchTile( - metadataKey: MetadataKey.backupRequireCharging, + metadataKey: SettingsKey.backupRequireCharging, selector: (c) => c.backup.requireCharging, titleKey: "charging", subtitleKey: "charging_requirement_mobile_backup", @@ -282,11 +282,11 @@ class _BackupDelaySlider extends ConsumerWidget { value: currentValue.toDouble(), onChanged: (double v) async { final seconds = backupDelayToSeconds(v.toInt()); - await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds); + await ref.read(settingsProvider).write(SettingsKey.backupTriggerDelay, seconds); }, onChangeEnd: (double v) async { final seconds = backupDelayToSeconds(v.toInt()); - await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds); + await ref.read(settingsProvider).write(SettingsKey.backupTriggerDelay, seconds); }, max: 3.0, min: 0.0, diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index bd5d05d02e..492a13de93 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -1,11 +1,11 @@ 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/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart'; class ExternalNetworkPreference extends HookConsumerWidget { @@ -26,7 +26,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { .map((e) => e.url) .toList(); - ref.read(metadataProvider).write(MetadataKey.networkExternalEndpointList, urls); + ref.read(settingsProvider).write(SettingsKey.networkExternalEndpointList, urls); } updateValidationStatus(String url, int index, AuxCheckStatus status) { @@ -64,7 +64,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { } useEffect(() { - final urls = ref.read(metadataProvider).systemConfig.network.externalEndpointList; + final urls = ref.read(appConfigProvider).network.externalEndpointList; if (urls.isEmpty) { return null; diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index f232f41a5d..7e6e169de7 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart'; @@ -19,9 +19,9 @@ class NetworkingSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currentEndpoint = getServerUrl(); - final featureEnabled = useState(ref.read(systemConfigProvider).network.autoEndpointSwitching); + final featureEnabled = useState(ref.read(appConfigProvider).network.autoEndpointSwitching); useValueChanged(featureEnabled.value, (_, __) { - ref.read(metadataProvider).write(.networkAutoEndpointSwitching, featureEnabled.value); + ref.read(settingsProvider).write(.networkAutoEndpointSwitching, featureEnabled.value); }); Future checkWifiReadPermission() async { diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 330555ed54..48d0ca672b 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; @@ -26,16 +26,16 @@ class PrimaryColorSetting extends HookConsumerWidget { } onUseSystemColorChange(bool newValue) { - ref.read(metadataProvider).write(.themeDynamic, newValue); + ref.read(settingsProvider).write(.themeDynamic, newValue); popBottomSheet(); } onPrimaryColorChange(ImmichColorPreset colorPreset) { - ref.read(metadataProvider).write(.themePrimaryColor, colorPreset); + ref.read(settingsProvider).write(.themePrimaryColor, colorPreset); //turn off system color setting if (themeConfig.dynamicTheme) { - ref.read(metadataProvider).write(.themeDynamic, false); + ref.read(settingsProvider).write(.themeDynamic, false); } popBottomSheet(); } diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index d71842d786..ffeeceae02 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -22,7 +22,7 @@ class ThemeSetting extends HookConsumerWidget { void onThemeChange(bool isDark) { currentTheme.value = isDark ? ThemeMode.dark : ThemeMode.light; - ref.read(metadataProvider).write(.themeMode, currentTheme.value); + ref.read(settingsProvider).write(.themeMode, currentTheme.value); } void onSystemThemeChange(bool isSystem) { @@ -39,11 +39,11 @@ class ThemeSetting extends HookConsumerWidget { currentTheme.value = ThemeMode.dark; } } - ref.read(metadataProvider).write(.themeMode, currentTheme.value); + ref.read(settingsProvider).write(.themeMode, currentTheme.value); } void onSurfaceColorSettingChange(bool useColorfulInterface) { - ref.read(metadataProvider).write(.themeColorfulInterface, useColorfulInterface); + ref.read(settingsProvider).write(.themeColorfulInterface, useColorfulInterface); colorfulInterface.value = useColorfulInterface; } diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 23987073dd..e92b885904 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -103,12 +103,16 @@ Class | Method | HTTP request | Description *AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset *AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset +*AssetsApi* | [**endSession**](doc//AssetsApi.md#endsession) | **DELETE** /assets/{id}/video/stream/{sessionId} | End HLS streaming session *AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key *AssetsApi* | [**getAssetOcr**](doc//AssetsApi.md#getassetocr) | **GET** /assets/{id}/ocr | Retrieve asset OCR data *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics +*AssetsApi* | [**getMainPlaylist**](doc//AssetsApi.md#getmainplaylist) | **GET** /assets/{id}/video/stream/main.m3u8 | Get HLS main playlist +*AssetsApi* | [**getMediaPlaylist**](doc//AssetsApi.md#getmediaplaylist) | **GET** /assets/{id}/video/stream/{sessionId}/{variantIndex}/playlist.m3u8 | Get HLS media playlist +*AssetsApi* | [**getSegment**](doc//AssetsApi.md#getsegment) | **GET** /assets/{id}/video/stream/{sessionId}/{variantIndex}/{filename} | Get HLS segment or init file *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video *AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job @@ -513,6 +517,9 @@ Class | Method | HTTP request | Description - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) + - [ReleaseChannel](doc//ReleaseChannel.md) + - [ReleaseEventV1](doc//ReleaseEventV1.md) + - [ReleaseType](doc//ReleaseType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - [RotateParameters](doc//RotateParameters.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) @@ -597,6 +604,7 @@ Class | Method | HTTP request | Description - [SystemConfigBackupsDto](doc//SystemConfigBackupsDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) + - [SystemConfigFFmpegRealtimeDto](doc//SystemConfigFFmpegRealtimeDto.md) - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) - [SystemConfigGeneratedFullsizeImageDto](doc//SystemConfigGeneratedFullsizeImageDto.md) - [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d5a6f483dc..3af18e5fe8 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -258,6 +258,9 @@ part 'model/ratings_response.dart'; part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; +part 'model/release_channel.dart'; +part 'model/release_event_v1.dart'; +part 'model/release_type.dart'; part 'model/reverse_geocoding_state_response_dto.dart'; part 'model/rotate_parameters.dart'; part 'model/search_album_response_dto.dart'; @@ -342,6 +345,7 @@ part 'model/sync_user_v1.dart'; part 'model/system_config_backups_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; +part 'model/system_config_f_fmpeg_realtime_dto.dart'; part 'model/system_config_faces_dto.dart'; part 'model/system_config_generated_fullsize_image_dto.dart'; part 'model/system_config_generated_image_dto.dart'; diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index e0a393948c..490c418785 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -25,7 +25,7 @@ class ActivitiesApi { /// Parameters: /// /// * [ActivityCreateDto] activityCreateDto (required): - Future createActivityWithHttpInfo(ActivityCreateDto activityCreateDto,) async { + Future createActivityWithHttpInfo(ActivityCreateDto activityCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities'; @@ -47,6 +47,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class ActivitiesApi { /// Parameters: /// /// * [ActivityCreateDto] activityCreateDto (required): - Future createActivity(ActivityCreateDto activityCreateDto,) async { - final response = await createActivityWithHttpInfo(activityCreateDto,); + Future createActivity(ActivityCreateDto activityCreateDto, { Future? abortTrigger, }) async { + final response = await createActivityWithHttpInfo(activityCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class ActivitiesApi { /// Parameters: /// /// * [String] id (required): - Future deleteActivityWithHttpInfo(String id,) async { + Future deleteActivityWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class ActivitiesApi { /// Parameters: /// /// * [String] id (required): - Future deleteActivity(String id,) async { - final response = await deleteActivityWithHttpInfo(id,); + Future deleteActivity(String id, { Future? abortTrigger, }) async { + final response = await deleteActivityWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -141,7 +143,7 @@ class ActivitiesApi { /// /// * [String] userId: /// Filter by user ID - Future getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async { + Future getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities'; @@ -177,6 +179,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -198,8 +201,8 @@ class ActivitiesApi { /// /// * [String] userId: /// Filter by user ID - Future?> getActivities(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async { - final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, level: level, type: type, userId: userId, ); + Future?> getActivities(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, Future? abortTrigger, }) async { + final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, level: level, type: type, userId: userId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -229,7 +232,7 @@ class ActivitiesApi { /// /// * [String] assetId: /// Asset ID (if activity is for an asset) - Future getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, }) async { + Future getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities/statistics'; @@ -256,6 +259,7 @@ class ActivitiesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -270,8 +274,8 @@ class ActivitiesApi { /// /// * [String] assetId: /// Asset ID (if activity is for an asset) - Future getActivityStatistics(String albumId, { String? assetId, }) async { - final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, ); + Future getActivityStatistics(String albumId, { String? assetId, Future? abortTrigger, }) async { + final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e0fc383c1d..e6e7cdbf40 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -27,7 +27,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -50,6 +50,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto,) async { - final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto,); + Future?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto,) async { + Future addAssetsToAlbumsWithHttpInfo(AlbumsAddAssetsDto albumsAddAssetsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/assets'; @@ -111,6 +112,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -121,8 +123,8 @@ class AlbumsApi { /// Parameters: /// /// * [AlbumsAddAssetsDto] albumsAddAssetsDto (required): - Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto,) async { - final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto,); + Future addAssetsToAlbums(AlbumsAddAssetsDto albumsAddAssetsDto, { Future? abortTrigger, }) async { + final response = await addAssetsToAlbumsWithHttpInfo(albumsAddAssetsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -147,7 +149,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [AddUsersDto] addUsersDto (required): - Future addUsersToAlbumWithHttpInfo(String id, AddUsersDto addUsersDto,) async { + Future addUsersToAlbumWithHttpInfo(String id, AddUsersDto addUsersDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/users' .replaceAll('{id}', id); @@ -170,6 +172,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -182,8 +185,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [AddUsersDto] addUsersDto (required): - Future addUsersToAlbum(String id, AddUsersDto addUsersDto,) async { - final response = await addUsersToAlbumWithHttpInfo(id, addUsersDto,); + Future addUsersToAlbum(String id, AddUsersDto addUsersDto, { Future? abortTrigger, }) async { + final response = await addUsersToAlbumWithHttpInfo(id, addUsersDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -206,7 +209,7 @@ class AlbumsApi { /// Parameters: /// /// * [CreateAlbumDto] createAlbumDto (required): - Future createAlbumWithHttpInfo(CreateAlbumDto createAlbumDto,) async { + Future createAlbumWithHttpInfo(CreateAlbumDto createAlbumDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums'; @@ -228,6 +231,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -238,8 +242,8 @@ class AlbumsApi { /// Parameters: /// /// * [CreateAlbumDto] createAlbumDto (required): - Future createAlbum(CreateAlbumDto createAlbumDto,) async { - final response = await createAlbumWithHttpInfo(createAlbumDto,); + Future createAlbum(CreateAlbumDto createAlbumDto, { Future? abortTrigger, }) async { + final response = await createAlbumWithHttpInfo(createAlbumDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -262,7 +266,7 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): - Future deleteAlbumWithHttpInfo(String id,) async { + Future deleteAlbumWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -285,6 +289,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -295,8 +300,8 @@ class AlbumsApi { /// Parameters: /// /// * [String] id (required): - Future deleteAlbum(String id,) async { - final response = await deleteAlbumWithHttpInfo(id,); + Future deleteAlbum(String id, { Future? abortTrigger, }) async { + final response = await deleteAlbumWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -315,7 +320,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, }) async { + Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -345,6 +350,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -359,8 +365,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future getAlbumInfo(String id, { String? key, String? slug, }) async { - final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, ); + Future getAlbumInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -387,7 +393,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, }) async { + Future getAlbumMapMarkersWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/map-markers' .replaceAll('{id}', id); @@ -417,6 +423,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -431,8 +438,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - Future?> getAlbumMapMarkers(String id, { String? key, String? slug, }) async { - final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, ); + Future?> getAlbumMapMarkers(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getAlbumMapMarkersWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -454,7 +461,7 @@ class AlbumsApi { /// Returns statistics about the albums available to the authenticated user. /// /// Note: This method returns the HTTP [Response]. - Future getAlbumStatisticsWithHttpInfo() async { + Future getAlbumStatisticsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/statistics'; @@ -476,14 +483,15 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve album statistics /// /// Returns statistics about the albums available to the authenticated user. - Future getAlbumStatistics() async { - final response = await getAlbumStatisticsWithHttpInfo(); + Future getAlbumStatistics({ Future? abortTrigger, }) async { + final response = await getAlbumStatisticsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -508,12 +516,18 @@ class AlbumsApi { /// * [String] assetId: /// Filter albums containing this asset ID (ignores other parameters) /// + /// * [String] id: + /// Album ID + /// /// * [bool] isOwned: /// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter /// /// * [bool] isShared: /// Filter by shared status: true = only shared, false = not shared, undefined = no filter - Future getAllAlbumsWithHttpInfo({ String? assetId, bool? isOwned, bool? isShared, }) async { + /// + /// * [String] name: + /// Album name (exact match) + Future getAllAlbumsWithHttpInfo({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums'; @@ -527,12 +541,18 @@ class AlbumsApi { if (assetId != null) { queryParams.addAll(_queryParams('', 'assetId', assetId)); } + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } if (isOwned != null) { queryParams.addAll(_queryParams('', 'isOwned', isOwned)); } if (isShared != null) { queryParams.addAll(_queryParams('', 'isShared', isShared)); } + if (name != null) { + queryParams.addAll(_queryParams('', 'name', name)); + } const contentTypes = []; @@ -545,6 +565,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -557,13 +578,19 @@ class AlbumsApi { /// * [String] assetId: /// Filter albums containing this asset ID (ignores other parameters) /// + /// * [String] id: + /// Album ID + /// /// * [bool] isOwned: /// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter /// /// * [bool] isShared: /// Filter by shared status: true = only shared, false = not shared, undefined = no filter - Future?> getAllAlbums({ String? assetId, bool? isOwned, bool? isShared, }) async { - final response = await getAllAlbumsWithHttpInfo( assetId: assetId, isOwned: isOwned, isShared: isShared, ); + /// + /// * [String] name: + /// Album name (exact match) + Future?> getAllAlbums({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, Future? abortTrigger, }) async { + final response = await getAllAlbumsWithHttpInfo(assetId: assetId, id: id, isOwned: isOwned, isShared: isShared, name: name, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -591,7 +618,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future removeAssetFromAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future removeAssetFromAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/assets' .replaceAll('{id}', id); @@ -614,6 +641,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -626,8 +654,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> removeAssetFromAlbum(String id, BulkIdsDto bulkIdsDto,) async { - final response = await removeAssetFromAlbumWithHttpInfo(id, bulkIdsDto,); + Future?> removeAssetFromAlbum(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await removeAssetFromAlbumWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -655,7 +683,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [String] userId (required): - Future removeUserFromAlbumWithHttpInfo(String id, String userId,) async { + Future removeUserFromAlbumWithHttpInfo(String id, String userId, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/user/{userId}' .replaceAll('{id}', id) @@ -679,6 +707,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -691,8 +720,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [String] userId (required): - Future removeUserFromAlbum(String id, String userId,) async { - final response = await removeUserFromAlbumWithHttpInfo(id, userId,); + Future removeUserFromAlbum(String id, String userId, { Future? abortTrigger, }) async { + final response = await removeUserFromAlbumWithHttpInfo(id, userId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -709,7 +738,7 @@ class AlbumsApi { /// * [String] id (required): /// /// * [UpdateAlbumDto] updateAlbumDto (required): - Future updateAlbumInfoWithHttpInfo(String id, UpdateAlbumDto updateAlbumDto,) async { + Future updateAlbumInfoWithHttpInfo(String id, UpdateAlbumDto updateAlbumDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -732,6 +761,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -744,8 +774,8 @@ class AlbumsApi { /// * [String] id (required): /// /// * [UpdateAlbumDto] updateAlbumDto (required): - Future updateAlbumInfo(String id, UpdateAlbumDto updateAlbumDto,) async { - final response = await updateAlbumInfoWithHttpInfo(id, updateAlbumDto,); + Future updateAlbumInfo(String id, UpdateAlbumDto updateAlbumDto, { Future? abortTrigger, }) async { + final response = await updateAlbumInfoWithHttpInfo(id, updateAlbumDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -772,7 +802,7 @@ class AlbumsApi { /// * [String] userId (required): /// /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): - Future updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto,) async { + Future updateAlbumUserWithHttpInfo(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}/user/{userId}' .replaceAll('{id}', id) @@ -796,6 +826,7 @@ class AlbumsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -810,8 +841,8 @@ class AlbumsApi { /// * [String] userId (required): /// /// * [UpdateAlbumUserDto] updateAlbumUserDto (required): - Future updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto,) async { - final response = await updateAlbumUserWithHttpInfo(id, userId, updateAlbumUserDto,); + Future updateAlbumUser(String id, String userId, UpdateAlbumUserDto updateAlbumUserDto, { Future? abortTrigger, }) async { + final response = await updateAlbumUserWithHttpInfo(id, userId, updateAlbumUserDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/api_keys_api.dart b/mobile/openapi/lib/api/api_keys_api.dart index 3ca85265c4..c26ddc263d 100644 --- a/mobile/openapi/lib/api/api_keys_api.dart +++ b/mobile/openapi/lib/api/api_keys_api.dart @@ -25,7 +25,7 @@ class APIKeysApi { /// Parameters: /// /// * [ApiKeyCreateDto] apiKeyCreateDto (required): - Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto,) async { + Future createApiKeyWithHttpInfo(ApiKeyCreateDto apiKeyCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; @@ -47,6 +47,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class APIKeysApi { /// Parameters: /// /// * [ApiKeyCreateDto] apiKeyCreateDto (required): - Future createApiKey(ApiKeyCreateDto apiKeyCreateDto,) async { - final response = await createApiKeyWithHttpInfo(apiKeyCreateDto,); + Future createApiKey(ApiKeyCreateDto apiKeyCreateDto, { Future? abortTrigger, }) async { + final response = await createApiKeyWithHttpInfo(apiKeyCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future deleteApiKeyWithHttpInfo(String id,) async { + Future deleteApiKeyWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future deleteApiKey(String id,) async { - final response = await deleteApiKeyWithHttpInfo(id,); + Future deleteApiKey(String id, { Future? abortTrigger, }) async { + final response = await deleteApiKeyWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -130,7 +132,7 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKeyWithHttpInfo(String id,) async { + Future getApiKeyWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); @@ -153,6 +155,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -163,8 +166,8 @@ class APIKeysApi { /// Parameters: /// /// * [String] id (required): - Future getApiKey(String id,) async { - final response = await getApiKeyWithHttpInfo(id,); + Future getApiKey(String id, { Future? abortTrigger, }) async { + final response = await getApiKeyWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -183,7 +186,7 @@ class APIKeysApi { /// Retrieve all API keys of the current user. /// /// Note: This method returns the HTTP [Response]. - Future getApiKeysWithHttpInfo() async { + Future getApiKeysWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys'; @@ -205,14 +208,15 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List all API keys /// /// Retrieve all API keys of the current user. - Future?> getApiKeys() async { - final response = await getApiKeysWithHttpInfo(); + Future?> getApiKeys({ Future? abortTrigger, }) async { + final response = await getApiKeysWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -234,7 +238,7 @@ class APIKeysApi { /// Retrieve the API key that is used to access this endpoint. /// /// Note: This method returns the HTTP [Response]. - Future getMyApiKeyWithHttpInfo() async { + Future getMyApiKeyWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/me'; @@ -256,14 +260,15 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve the current API key /// /// Retrieve the API key that is used to access this endpoint. - Future getMyApiKey() async { - final response = await getMyApiKeyWithHttpInfo(); + Future getMyApiKey({ Future? abortTrigger, }) async { + final response = await getMyApiKeyWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -288,7 +293,7 @@ class APIKeysApi { /// * [String] id (required): /// /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): - Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { + Future updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/api-keys/{id}' .replaceAll('{id}', id); @@ -311,6 +316,7 @@ class APIKeysApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -323,8 +329,8 @@ class APIKeysApi { /// * [String] id (required): /// /// * [ApiKeyUpdateDto] apiKeyUpdateDto (required): - Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto,) async { - final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto,); + Future updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto, { Future? abortTrigger, }) async { + final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 691c57cd3e..61d3f599cb 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -25,7 +25,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required): - Future checkBulkUploadWithHttpInfo(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async { + Future checkBulkUploadWithHttpInfo(AssetBulkUploadCheckDto assetBulkUploadCheckDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/bulk-upload-check'; @@ -47,6 +47,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUploadCheckDto] assetBulkUploadCheckDto (required): - Future checkBulkUpload(AssetBulkUploadCheckDto assetBulkUploadCheckDto,) async { - final response = await checkBulkUploadWithHttpInfo(assetBulkUploadCheckDto,); + Future checkBulkUpload(AssetBulkUploadCheckDto assetBulkUploadCheckDto, { Future? abortTrigger, }) async { + final response = await checkBulkUploadWithHttpInfo(assetBulkUploadCheckDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetCopyDto] assetCopyDto (required): - Future copyAssetWithHttpInfo(AssetCopyDto assetCopyDto,) async { + Future copyAssetWithHttpInfo(AssetCopyDto assetCopyDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/copy'; @@ -103,6 +104,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetCopyDto] assetCopyDto (required): - Future copyAsset(AssetCopyDto assetCopyDto,) async { - final response = await copyAssetWithHttpInfo(assetCopyDto,); + Future copyAsset(AssetCopyDto assetCopyDto, { Future? abortTrigger, }) async { + final response = await copyAssetWithHttpInfo(assetCopyDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -133,7 +135,7 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future deleteAssetMetadataWithHttpInfo(String id, String key,) async { + Future deleteAssetMetadataWithHttpInfo(String id, String key, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' .replaceAll('{id}', id) @@ -157,6 +159,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -171,8 +174,8 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future deleteAssetMetadata(String id, String key,) async { - final response = await deleteAssetMetadataWithHttpInfo(id, key,); + Future deleteAssetMetadata(String id, String key, { Future? abortTrigger, }) async { + final response = await deleteAssetMetadataWithHttpInfo(id, key, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -187,7 +190,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkDeleteDto] assetBulkDeleteDto (required): - Future deleteAssetsWithHttpInfo(AssetBulkDeleteDto assetBulkDeleteDto,) async { + Future deleteAssetsWithHttpInfo(AssetBulkDeleteDto assetBulkDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -209,6 +212,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -219,8 +223,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkDeleteDto] assetBulkDeleteDto (required): - Future deleteAssets(AssetBulkDeleteDto assetBulkDeleteDto,) async { - final response = await deleteAssetsWithHttpInfo(assetBulkDeleteDto,); + Future deleteAssets(AssetBulkDeleteDto assetBulkDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteAssetsWithHttpInfo(assetBulkDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -235,7 +239,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required): - Future deleteBulkAssetMetadataWithHttpInfo(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async { + Future deleteBulkAssetMetadataWithHttpInfo(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/metadata'; @@ -257,6 +261,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -267,8 +272,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required): - Future deleteBulkAssetMetadata(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async { - final response = await deleteBulkAssetMetadataWithHttpInfo(assetMetadataBulkDeleteDto,); + Future deleteBulkAssetMetadata(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteBulkAssetMetadataWithHttpInfo(assetMetadataBulkDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -290,7 +295,7 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future downloadAssetWithHttpInfo(String id, { bool? edited, String? key, String? slug, }) async { + Future downloadAssetWithHttpInfo(String id, { bool? edited, String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/original' .replaceAll('{id}', id); @@ -323,6 +328,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -340,8 +346,8 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future downloadAsset(String id, { bool? edited, String? key, String? slug, }) async { - final response = await downloadAssetWithHttpInfo(id, edited: edited, key: key, slug: slug, ); + Future downloadAsset(String id, { bool? edited, String? key, String? slug, Future? abortTrigger, }) async { + final response = await downloadAssetWithHttpInfo(id, edited: edited, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -366,7 +372,7 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetEditsCreateDto] assetEditsCreateDto (required): - Future editAssetWithHttpInfo(String id, AssetEditsCreateDto assetEditsCreateDto,) async { + Future editAssetWithHttpInfo(String id, AssetEditsCreateDto assetEditsCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); @@ -389,6 +395,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -401,8 +408,8 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetEditsCreateDto] assetEditsCreateDto (required): - Future editAsset(String id, AssetEditsCreateDto assetEditsCreateDto,) async { - final response = await editAssetWithHttpInfo(id, assetEditsCreateDto,); + Future editAsset(String id, AssetEditsCreateDto assetEditsCreateDto, { Future? abortTrigger, }) async { + final response = await editAssetWithHttpInfo(id, assetEditsCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -416,6 +423,76 @@ class AssetsApi { return null; } + /// End HLS streaming session + /// + /// Releases server resources for the streaming session. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future endSessionWithHttpInfo(String id, String sessionId, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/{sessionId}' + .replaceAll('{id}', id) + .replaceAll('{sessionId}', sessionId); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// End HLS streaming session + /// + /// Releases server resources for the streaming session. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future endSession(String id, String sessionId, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await endSessionWithHttpInfo(id, sessionId, key: key, slug: slug, abortTrigger: abortTrigger,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Retrieve edits for an existing asset /// /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. @@ -425,7 +502,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetEditsWithHttpInfo(String id,) async { + Future getAssetEditsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); @@ -448,6 +525,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -458,8 +536,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetEdits(String id,) async { - final response = await getAssetEditsWithHttpInfo(id,); + Future getAssetEdits(String id, { Future? abortTrigger, }) async { + final response = await getAssetEditsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -486,7 +564,7 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future getAssetInfoWithHttpInfo(String id, { String? key, String? slug, }) async { + Future getAssetInfoWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}' .replaceAll('{id}', id); @@ -516,6 +594,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -530,8 +609,8 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future getAssetInfo(String id, { String? key, String? slug, }) async { - final response = await getAssetInfoWithHttpInfo(id, key: key, slug: slug, ); + Future getAssetInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getAssetInfoWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -554,7 +633,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetMetadataWithHttpInfo(String id,) async { + Future getAssetMetadataWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata' .replaceAll('{id}', id); @@ -577,6 +656,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -587,8 +667,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future?> getAssetMetadata(String id,) async { - final response = await getAssetMetadataWithHttpInfo(id,); + Future?> getAssetMetadata(String id, { Future? abortTrigger, }) async { + final response = await getAssetMetadataWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -618,7 +698,7 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future getAssetMetadataByKeyWithHttpInfo(String id, String key,) async { + Future getAssetMetadataByKeyWithHttpInfo(String id, String key, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' .replaceAll('{id}', id) @@ -642,6 +722,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -656,8 +737,8 @@ class AssetsApi { /// /// * [String] key (required): /// Metadata key - Future getAssetMetadataByKey(String id, String key,) async { - final response = await getAssetMetadataByKeyWithHttpInfo(id, key,); + Future getAssetMetadataByKey(String id, String key, { Future? abortTrigger, }) async { + final response = await getAssetMetadataByKeyWithHttpInfo(id, key, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -680,7 +761,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetOcrWithHttpInfo(String id,) async { + Future getAssetOcrWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/ocr' .replaceAll('{id}', id); @@ -703,6 +784,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -713,8 +795,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future?> getAssetOcr(String id,) async { - final response = await getAssetOcrWithHttpInfo(id,); + Future?> getAssetOcr(String id, { Future? abortTrigger, }) async { + final response = await getAssetOcrWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -746,7 +828,7 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { + Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/statistics'; @@ -778,6 +860,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -794,8 +877,8 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { - final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); + Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { + final response = await getAssetStatisticsWithHttpInfo(isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -809,6 +892,250 @@ class AssetsApi { return null; } + /// Get HLS main playlist + /// + /// Returns an HLS main playlist with all available variants for the asset. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMainPlaylistWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/main.m3u8' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// Get HLS main playlist + /// + /// Returns an HLS main playlist with all available variants for the asset. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMainPlaylist(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getMainPlaylistWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); + 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), 'String',) as String; + + } + return null; + } + + /// Get HLS media playlist + /// + /// Returns an HLS media playlist for one variant of the streaming session. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMediaPlaylistWithHttpInfo(String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/{sessionId}/{variantIndex}/playlist.m3u8' + .replaceAll('{id}', id) + .replaceAll('{sessionId}', sessionId) + .replaceAll('{variantIndex}', variantIndex.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// Get HLS media playlist + /// + /// Returns an HLS media playlist for one variant of the streaming session. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getMediaPlaylist(String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getMediaPlaylistWithHttpInfo(id, sessionId, variantIndex, key: key, slug: slug, abortTrigger: abortTrigger,); + 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), 'String',) as String; + + } + return null; + } + + /// Get HLS segment or init file + /// + /// Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s). + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] filename (required): + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getSegmentWithHttpInfo(String filename, String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/video/stream/{sessionId}/{variantIndex}/{filename}' + .replaceAll('{filename}', filename) + .replaceAll('{id}', id) + .replaceAll('{sessionId}', sessionId) + .replaceAll('{variantIndex}', variantIndex.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, + ); + } + + /// Get HLS segment or init file + /// + /// Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s). + /// + /// Parameters: + /// + /// * [String] filename (required): + /// + /// * [String] id (required): + /// + /// * [String] sessionId (required): + /// + /// * [int] variantIndex (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future getSegment(String filename, String id, String sessionId, int variantIndex, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getSegmentWithHttpInfo(filename, id, sessionId, variantIndex, key: key, slug: slug, abortTrigger: abortTrigger,); + 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), 'MultipartFile',) as MultipartFile; + + } + return null; + } + /// Play asset video /// /// Streams the video file for the specified asset. This endpoint also supports byte range requests. @@ -822,7 +1149,7 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future playAssetVideoWithHttpInfo(String id, { String? key, String? slug, }) async { + Future playAssetVideoWithHttpInfo(String id, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/video/playback' .replaceAll('{id}', id); @@ -852,6 +1179,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -866,8 +1194,8 @@ class AssetsApi { /// * [String] key: /// /// * [String] slug: - Future playAssetVideo(String id, { String? key, String? slug, }) async { - final response = await playAssetVideoWithHttpInfo(id, key: key, slug: slug, ); + Future playAssetVideo(String id, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await playAssetVideoWithHttpInfo(id, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -890,7 +1218,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future removeAssetEditsWithHttpInfo(String id,) async { + Future removeAssetEditsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); @@ -913,6 +1241,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -923,8 +1252,8 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future removeAssetEdits(String id,) async { - final response = await removeAssetEditsWithHttpInfo(id,); + Future removeAssetEdits(String id, { Future? abortTrigger, }) async { + final response = await removeAssetEditsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -939,7 +1268,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetJobsDto] assetJobsDto (required): - Future runAssetJobsWithHttpInfo(AssetJobsDto assetJobsDto,) async { + Future runAssetJobsWithHttpInfo(AssetJobsDto assetJobsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/jobs'; @@ -961,6 +1290,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -971,8 +1301,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetJobsDto] assetJobsDto (required): - Future runAssetJobs(AssetJobsDto assetJobsDto,) async { - final response = await runAssetJobsWithHttpInfo(assetJobsDto,); + Future runAssetJobs(AssetJobsDto assetJobsDto, { Future? abortTrigger, }) async { + final response = await runAssetJobsWithHttpInfo(assetJobsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -989,7 +1319,7 @@ class AssetsApi { /// * [String] id (required): /// /// * [UpdateAssetDto] updateAssetDto (required): - Future updateAssetWithHttpInfo(String id, UpdateAssetDto updateAssetDto,) async { + Future updateAssetWithHttpInfo(String id, UpdateAssetDto updateAssetDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}' .replaceAll('{id}', id); @@ -1012,6 +1342,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1024,8 +1355,8 @@ class AssetsApi { /// * [String] id (required): /// /// * [UpdateAssetDto] updateAssetDto (required): - Future updateAsset(String id, UpdateAssetDto updateAssetDto,) async { - final response = await updateAssetWithHttpInfo(id, updateAssetDto,); + Future updateAsset(String id, UpdateAssetDto updateAssetDto, { Future? abortTrigger, }) async { + final response = await updateAssetWithHttpInfo(id, updateAssetDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1050,7 +1381,7 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): - Future updateAssetMetadataWithHttpInfo(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { + Future updateAssetMetadataWithHttpInfo(String id, AssetMetadataUpsertDto assetMetadataUpsertDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata' .replaceAll('{id}', id); @@ -1073,6 +1404,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1085,8 +1417,8 @@ class AssetsApi { /// * [String] id (required): /// /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): - Future?> updateAssetMetadata(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { - final response = await updateAssetMetadataWithHttpInfo(id, assetMetadataUpsertDto,); + Future?> updateAssetMetadata(String id, AssetMetadataUpsertDto assetMetadataUpsertDto, { Future? abortTrigger, }) async { + final response = await updateAssetMetadataWithHttpInfo(id, assetMetadataUpsertDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1112,7 +1444,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUpdateDto] assetBulkUpdateDto (required): - Future updateAssetsWithHttpInfo(AssetBulkUpdateDto assetBulkUpdateDto,) async { + Future updateAssetsWithHttpInfo(AssetBulkUpdateDto assetBulkUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1134,6 +1466,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1144,8 +1477,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetBulkUpdateDto] assetBulkUpdateDto (required): - Future updateAssets(AssetBulkUpdateDto assetBulkUpdateDto,) async { - final response = await updateAssetsWithHttpInfo(assetBulkUpdateDto,); + Future updateAssets(AssetBulkUpdateDto assetBulkUpdateDto, { Future? abortTrigger, }) async { + final response = await updateAssetsWithHttpInfo(assetBulkUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1160,7 +1493,7 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required): - Future updateBulkAssetMetadataWithHttpInfo(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async { + Future updateBulkAssetMetadataWithHttpInfo(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/metadata'; @@ -1182,6 +1515,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1192,8 +1526,8 @@ class AssetsApi { /// Parameters: /// /// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required): - Future?> updateBulkAssetMetadata(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async { - final response = await updateBulkAssetMetadataWithHttpInfo(assetMetadataBulkUpsertDto,); + Future?> updateBulkAssetMetadata(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto, { Future? abortTrigger, }) async { + final response = await updateBulkAssetMetadataWithHttpInfo(assetMetadataBulkUpsertDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1253,7 +1587,7 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1333,6 +1667,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1377,8 +1712,8 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); + Future uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, Future? abortTrigger, }) async { + final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1410,7 +1745,7 @@ class AssetsApi { /// * [AssetMediaSize] size: /// /// * [String] slug: - Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { + Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/thumbnail' .replaceAll('{id}', id); @@ -1446,6 +1781,7 @@ class AssetsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -1465,8 +1801,8 @@ class AssetsApi { /// * [AssetMediaSize] size: /// /// * [String] slug: - Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { - final response = await viewAssetWithHttpInfo(id, edited: edited, key: key, size: size, slug: slug, ); + Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, Future? abortTrigger, }) async { + final response = await viewAssetWithHttpInfo(id, edited: edited, key: key, size: size, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/authentication_admin_api.dart b/mobile/openapi/lib/api/authentication_admin_api.dart index 0a4b91ebc3..2c107891b3 100644 --- a/mobile/openapi/lib/api/authentication_admin_api.dart +++ b/mobile/openapi/lib/api/authentication_admin_api.dart @@ -21,7 +21,7 @@ class AuthenticationAdminApi { /// Unlinks all OAuth accounts associated with user accounts in the system. /// /// Note: This method returns the HTTP [Response]. - Future unlinkAllOAuthAccountsAdminWithHttpInfo() async { + Future unlinkAllOAuthAccountsAdminWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/auth/unlink-all'; @@ -43,14 +43,15 @@ class AuthenticationAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Unlink all OAuth accounts /// /// Unlinks all OAuth accounts associated with user accounts in the system. - Future unlinkAllOAuthAccountsAdmin() async { - final response = await unlinkAllOAuthAccountsAdminWithHttpInfo(); + Future unlinkAllOAuthAccountsAdmin({ Future? abortTrigger, }) async { + final response = await unlinkAllOAuthAccountsAdminWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index e1219f2c03..8e088d040b 100644 --- a/mobile/openapi/lib/api/authentication_api.dart +++ b/mobile/openapi/lib/api/authentication_api.dart @@ -25,7 +25,7 @@ class AuthenticationApi { /// Parameters: /// /// * [ChangePasswordDto] changePasswordDto (required): - Future changePasswordWithHttpInfo(ChangePasswordDto changePasswordDto,) async { + Future changePasswordWithHttpInfo(ChangePasswordDto changePasswordDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/change-password'; @@ -47,6 +47,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class AuthenticationApi { /// Parameters: /// /// * [ChangePasswordDto] changePasswordDto (required): - Future changePassword(ChangePasswordDto changePasswordDto,) async { - final response = await changePasswordWithHttpInfo(changePasswordDto,); + Future changePassword(ChangePasswordDto changePasswordDto, { Future? abortTrigger, }) async { + final response = await changePasswordWithHttpInfo(changePasswordDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeChangeDto] pinCodeChangeDto (required): - Future changePinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async { + Future changePinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/pin-code'; @@ -103,6 +104,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeChangeDto] pinCodeChangeDto (required): - Future changePinCode(PinCodeChangeDto pinCodeChangeDto,) async { - final response = await changePinCodeWithHttpInfo(pinCodeChangeDto,); + Future changePinCode(PinCodeChangeDto pinCodeChangeDto, { Future? abortTrigger, }) async { + final response = await changePinCodeWithHttpInfo(pinCodeChangeDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -129,7 +131,7 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future finishOAuthWithHttpInfo(OAuthCallbackDto oAuthCallbackDto,) async { + Future finishOAuthWithHttpInfo(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/callback'; @@ -151,6 +153,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -161,8 +164,8 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future finishOAuth(OAuthCallbackDto oAuthCallbackDto,) async { - final response = await finishOAuthWithHttpInfo(oAuthCallbackDto,); + Future finishOAuth(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { + final response = await finishOAuthWithHttpInfo(oAuthCallbackDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -181,7 +184,7 @@ class AuthenticationApi { /// Get information about the current session, including whether the user has a password, and if the session can access locked assets. /// /// Note: This method returns the HTTP [Response]. - Future getAuthStatusWithHttpInfo() async { + Future getAuthStatusWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/status'; @@ -203,14 +206,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve auth status /// /// Get information about the current session, including whether the user has a password, and if the session can access locked assets. - Future getAuthStatus() async { - final response = await getAuthStatusWithHttpInfo(); + Future getAuthStatus({ Future? abortTrigger, }) async { + final response = await getAuthStatusWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -233,7 +237,7 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future linkOAuthAccountWithHttpInfo(OAuthCallbackDto oAuthCallbackDto,) async { + Future linkOAuthAccountWithHttpInfo(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/link'; @@ -255,6 +259,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -265,8 +270,8 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthCallbackDto] oAuthCallbackDto (required): - Future linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto,) async { - final response = await linkOAuthAccountWithHttpInfo(oAuthCallbackDto,); + Future linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto, { Future? abortTrigger, }) async { + final response = await linkOAuthAccountWithHttpInfo(oAuthCallbackDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -285,7 +290,7 @@ class AuthenticationApi { /// Remove elevated access to locked assets from the current session. /// /// Note: This method returns the HTTP [Response]. - Future lockAuthSessionWithHttpInfo() async { + Future lockAuthSessionWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/session/lock'; @@ -307,14 +312,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Lock auth session /// /// Remove elevated access to locked assets from the current session. - Future lockAuthSession() async { - final response = await lockAuthSessionWithHttpInfo(); + Future lockAuthSession({ Future? abortTrigger, }) async { + final response = await lockAuthSessionWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -329,7 +335,7 @@ class AuthenticationApi { /// Parameters: /// /// * [LoginCredentialDto] loginCredentialDto (required): - Future loginWithHttpInfo(LoginCredentialDto loginCredentialDto,) async { + Future loginWithHttpInfo(LoginCredentialDto loginCredentialDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/login'; @@ -351,6 +357,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -361,8 +368,8 @@ class AuthenticationApi { /// Parameters: /// /// * [LoginCredentialDto] loginCredentialDto (required): - Future login(LoginCredentialDto loginCredentialDto,) async { - final response = await loginWithHttpInfo(loginCredentialDto,); + Future login(LoginCredentialDto loginCredentialDto, { Future? abortTrigger, }) async { + final response = await loginWithHttpInfo(loginCredentialDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -381,7 +388,7 @@ class AuthenticationApi { /// Logout the current user and invalidate the session token. /// /// Note: This method returns the HTTP [Response]. - Future logoutWithHttpInfo() async { + Future logoutWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/logout'; @@ -403,14 +410,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Logout /// /// Logout the current user and invalidate the session token. - Future logout() async { - final response = await logoutWithHttpInfo(); + Future logout({ Future? abortTrigger, }) async { + final response = await logoutWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -434,7 +442,7 @@ class AuthenticationApi { /// /// * [String] logoutToken (required): /// OAuth logout token - Future logoutOAuthWithHttpInfo(String logoutToken,) async { + Future logoutOAuthWithHttpInfo(String logoutToken, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/backchannel-logout'; @@ -459,6 +467,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -470,8 +479,8 @@ class AuthenticationApi { /// /// * [String] logoutToken (required): /// OAuth logout token - Future logoutOAuth(String logoutToken,) async { - final response = await logoutOAuthWithHttpInfo(logoutToken,); + Future logoutOAuth(String logoutToken, { Future? abortTrigger, }) async { + final response = await logoutOAuthWithHttpInfo(logoutToken, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -482,7 +491,7 @@ class AuthenticationApi { /// Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting. /// /// Note: This method returns the HTTP [Response]. - Future redirectOAuthToMobileWithHttpInfo() async { + Future redirectOAuthToMobileWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/mobile-redirect'; @@ -504,14 +513,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Redirect OAuth to mobile /// /// Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting. - Future redirectOAuthToMobile() async { - final response = await redirectOAuthToMobileWithHttpInfo(); + Future redirectOAuthToMobile({ Future? abortTrigger, }) async { + final response = await redirectOAuthToMobileWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -526,7 +536,7 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeResetDto] pinCodeResetDto (required): - Future resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto,) async { + Future resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/pin-code'; @@ -548,6 +558,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -558,8 +569,8 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeResetDto] pinCodeResetDto (required): - Future resetPinCode(PinCodeResetDto pinCodeResetDto,) async { - final response = await resetPinCodeWithHttpInfo(pinCodeResetDto,); + Future resetPinCode(PinCodeResetDto pinCodeResetDto, { Future? abortTrigger, }) async { + final response = await resetPinCodeWithHttpInfo(pinCodeResetDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -574,7 +585,7 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeSetupDto] pinCodeSetupDto (required): - Future setupPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto,) async { + Future setupPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/pin-code'; @@ -596,6 +607,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -606,8 +618,8 @@ class AuthenticationApi { /// Parameters: /// /// * [PinCodeSetupDto] pinCodeSetupDto (required): - Future setupPinCode(PinCodeSetupDto pinCodeSetupDto,) async { - final response = await setupPinCodeWithHttpInfo(pinCodeSetupDto,); + Future setupPinCode(PinCodeSetupDto pinCodeSetupDto, { Future? abortTrigger, }) async { + final response = await setupPinCodeWithHttpInfo(pinCodeSetupDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -622,7 +634,7 @@ class AuthenticationApi { /// Parameters: /// /// * [SignUpDto] signUpDto (required): - Future signUpAdminWithHttpInfo(SignUpDto signUpDto,) async { + Future signUpAdminWithHttpInfo(SignUpDto signUpDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/admin-sign-up'; @@ -644,6 +656,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -654,8 +667,8 @@ class AuthenticationApi { /// Parameters: /// /// * [SignUpDto] signUpDto (required): - Future signUpAdmin(SignUpDto signUpDto,) async { - final response = await signUpAdminWithHttpInfo(signUpDto,); + Future signUpAdmin(SignUpDto signUpDto, { Future? abortTrigger, }) async { + final response = await signUpAdminWithHttpInfo(signUpDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -678,7 +691,7 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthConfigDto] oAuthConfigDto (required): - Future startOAuthWithHttpInfo(OAuthConfigDto oAuthConfigDto,) async { + Future startOAuthWithHttpInfo(OAuthConfigDto oAuthConfigDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/authorize'; @@ -700,6 +713,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -710,8 +724,8 @@ class AuthenticationApi { /// Parameters: /// /// * [OAuthConfigDto] oAuthConfigDto (required): - Future startOAuth(OAuthConfigDto oAuthConfigDto,) async { - final response = await startOAuthWithHttpInfo(oAuthConfigDto,); + Future startOAuth(OAuthConfigDto oAuthConfigDto, { Future? abortTrigger, }) async { + final response = await startOAuthWithHttpInfo(oAuthConfigDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -730,7 +744,7 @@ class AuthenticationApi { /// Unlink the OAuth account from the authenticated user. /// /// Note: This method returns the HTTP [Response]. - Future unlinkOAuthAccountWithHttpInfo() async { + Future unlinkOAuthAccountWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/oauth/unlink'; @@ -752,14 +766,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Unlink OAuth account /// /// Unlink the OAuth account from the authenticated user. - Future unlinkOAuthAccount() async { - final response = await unlinkOAuthAccountWithHttpInfo(); + Future unlinkOAuthAccount({ Future? abortTrigger, }) async { + final response = await unlinkOAuthAccountWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -782,7 +797,7 @@ class AuthenticationApi { /// Parameters: /// /// * [SessionUnlockDto] sessionUnlockDto (required): - Future unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto,) async { + Future unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/session/unlock'; @@ -804,6 +819,7 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -814,8 +830,8 @@ class AuthenticationApi { /// Parameters: /// /// * [SessionUnlockDto] sessionUnlockDto (required): - Future unlockAuthSession(SessionUnlockDto sessionUnlockDto,) async { - final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto,); + Future unlockAuthSession(SessionUnlockDto sessionUnlockDto, { Future? abortTrigger, }) async { + final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -826,7 +842,7 @@ class AuthenticationApi { /// Validate the current authorization method is still valid. /// /// Note: This method returns the HTTP [Response]. - Future validateAccessTokenWithHttpInfo() async { + Future validateAccessTokenWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/auth/validateToken'; @@ -848,14 +864,15 @@ class AuthenticationApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Validate access token /// /// Validate the current authorization method is still valid. - Future validateAccessToken() async { - final response = await validateAccessTokenWithHttpInfo(); + Future validateAccessToken({ Future? abortTrigger, }) async { + final response = await validateAccessTokenWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart index 768185db1e..ba393833b5 100644 --- a/mobile/openapi/lib/api/database_backups_admin_api.dart +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -25,7 +25,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): - Future deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { + Future deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups'; @@ -47,6 +47,7 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): - Future deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { - final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto,); + Future deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -73,7 +74,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [String] filename (required): - Future downloadDatabaseBackupWithHttpInfo(String filename,) async { + Future downloadDatabaseBackupWithHttpInfo(String filename, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/{filename}' .replaceAll('{filename}', filename); @@ -96,6 +97,7 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -106,8 +108,8 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [String] filename (required): - Future downloadDatabaseBackup(String filename,) async { - final response = await downloadDatabaseBackupWithHttpInfo(filename,); + Future downloadDatabaseBackup(String filename, { Future? abortTrigger, }) async { + final response = await downloadDatabaseBackupWithHttpInfo(filename, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -126,7 +128,7 @@ class DatabaseBackupsAdminApi { /// Get the list of the successful and failed backups /// /// Note: This method returns the HTTP [Response]. - Future listDatabaseBackupsWithHttpInfo() async { + Future listDatabaseBackupsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups'; @@ -148,14 +150,15 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List database backups /// /// Get the list of the successful and failed backups - Future listDatabaseBackups() async { - final response = await listDatabaseBackupsWithHttpInfo(); + Future listDatabaseBackups({ Future? abortTrigger, }) async { + final response = await listDatabaseBackupsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -174,7 +177,7 @@ class DatabaseBackupsAdminApi { /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) /// /// Note: This method returns the HTTP [Response]. - Future startDatabaseRestoreFlowWithHttpInfo() async { + Future startDatabaseRestoreFlowWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/start-restore'; @@ -196,14 +199,15 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Start database backup restore flow /// /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) - Future startDatabaseRestoreFlow() async { - final response = await startDatabaseRestoreFlowWithHttpInfo(); + Future startDatabaseRestoreFlow({ Future? abortTrigger, }) async { + final response = await startDatabaseRestoreFlowWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -219,7 +223,7 @@ class DatabaseBackupsAdminApi { /// /// * [MultipartFile] file: /// Database backup file - Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { + Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/upload'; @@ -251,6 +255,7 @@ class DatabaseBackupsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -262,8 +267,8 @@ class DatabaseBackupsAdminApi { /// /// * [MultipartFile] file: /// Database backup file - Future uploadDatabaseBackup({ MultipartFile? file, }) async { - final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); + Future uploadDatabaseBackup({ MultipartFile? file, Future? abortTrigger, }) async { + final response = await uploadDatabaseBackupWithHttpInfo(file: file, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index a437cd5837..a8e2deab44 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -25,7 +25,7 @@ class DeprecatedApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecatedWithHttpInfo(String id,) async { + Future createPartnerDeprecatedWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class DeprecatedApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class DeprecatedApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecated(String id,) async { - final response = await createPartnerDeprecatedWithHttpInfo(id,); + Future createPartnerDeprecated(String id, { Future? abortTrigger, }) async { + final response = await createPartnerDeprecatedWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -78,7 +79,7 @@ class DeprecatedApi { /// Retrieve the counts of the current queue, as well as the current status. /// /// Note: This method returns the HTTP [Response]. - Future getQueuesLegacyWithHttpInfo() async { + Future getQueuesLegacyWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs'; @@ -100,14 +101,15 @@ class DeprecatedApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. - Future getQueuesLegacy() async { - final response = await getQueuesLegacyWithHttpInfo(); + Future getQueuesLegacy({ Future? abortTrigger, }) async { + final response = await getQueuesLegacyWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -132,7 +134,7 @@ class DeprecatedApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { + Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs/{name}' .replaceAll('{name}', name.toString()); @@ -155,6 +157,7 @@ class DeprecatedApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -167,8 +170,8 @@ class DeprecatedApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { - final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); + Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { + final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/download_api.dart b/mobile/openapi/lib/api/download_api.dart index 4d0c5c8165..ac26259277 100644 --- a/mobile/openapi/lib/api/download_api.dart +++ b/mobile/openapi/lib/api/download_api.dart @@ -29,7 +29,7 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future downloadArchiveWithHttpInfo(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, }) async { + Future downloadArchiveWithHttpInfo(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/download/archive'; @@ -58,6 +58,7 @@ class DownloadApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -72,8 +73,8 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future downloadArchive(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, }) async { - final response = await downloadArchiveWithHttpInfo(downloadArchiveDto, key: key, slug: slug, ); + Future downloadArchive(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await downloadArchiveWithHttpInfo(downloadArchiveDto, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -100,7 +101,7 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, }) async { + Future getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/download/info'; @@ -129,6 +130,7 @@ class DownloadApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -143,8 +145,8 @@ class DownloadApi { /// * [String] key: /// /// * [String] slug: - Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, }) async { - final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, slug: slug, ); + Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/duplicates_api.dart b/mobile/openapi/lib/api/duplicates_api.dart index 9bd01281b3..357947b889 100644 --- a/mobile/openapi/lib/api/duplicates_api.dart +++ b/mobile/openapi/lib/api/duplicates_api.dart @@ -25,7 +25,7 @@ class DuplicatesApi { /// Parameters: /// /// * [String] id (required): - Future deleteDuplicateWithHttpInfo(String id,) async { + Future deleteDuplicateWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class DuplicatesApi { /// Parameters: /// /// * [String] id (required): - Future deleteDuplicate(String id,) async { - final response = await deleteDuplicateWithHttpInfo(id,); + Future deleteDuplicate(String id, { Future? abortTrigger, }) async { + final response = await deleteDuplicateWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -74,7 +75,7 @@ class DuplicatesApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteDuplicatesWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future deleteDuplicatesWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates'; @@ -96,6 +97,7 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -106,8 +108,8 @@ class DuplicatesApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteDuplicates(BulkIdsDto bulkIdsDto,) async { - final response = await deleteDuplicatesWithHttpInfo(bulkIdsDto,); + Future deleteDuplicates(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await deleteDuplicatesWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -118,7 +120,7 @@ class DuplicatesApi { /// Retrieve a list of duplicate assets available to the authenticated user. /// /// Note: This method returns the HTTP [Response]. - Future getAssetDuplicatesWithHttpInfo() async { + Future getAssetDuplicatesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates'; @@ -140,14 +142,15 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve duplicates /// /// Retrieve a list of duplicate assets available to the authenticated user. - Future?> getAssetDuplicates() async { - final response = await getAssetDuplicatesWithHttpInfo(); + Future?> getAssetDuplicates({ Future? abortTrigger, }) async { + final response = await getAssetDuplicatesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -173,7 +176,7 @@ class DuplicatesApi { /// Parameters: /// /// * [DuplicateResolveDto] duplicateResolveDto (required): - Future resolveDuplicatesWithHttpInfo(DuplicateResolveDto duplicateResolveDto,) async { + Future resolveDuplicatesWithHttpInfo(DuplicateResolveDto duplicateResolveDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/duplicates/resolve'; @@ -195,6 +198,7 @@ class DuplicatesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -205,8 +209,8 @@ class DuplicatesApi { /// Parameters: /// /// * [DuplicateResolveDto] duplicateResolveDto (required): - Future?> resolveDuplicates(DuplicateResolveDto duplicateResolveDto,) async { - final response = await resolveDuplicatesWithHttpInfo(duplicateResolveDto,); + Future?> resolveDuplicates(DuplicateResolveDto duplicateResolveDto, { Future? abortTrigger, }) async { + final response = await resolveDuplicatesWithHttpInfo(duplicateResolveDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/faces_api.dart b/mobile/openapi/lib/api/faces_api.dart index 43d63b47b9..2a71bbbaca 100644 --- a/mobile/openapi/lib/api/faces_api.dart +++ b/mobile/openapi/lib/api/faces_api.dart @@ -25,7 +25,7 @@ class FacesApi { /// Parameters: /// /// * [AssetFaceCreateDto] assetFaceCreateDto (required): - Future createFaceWithHttpInfo(AssetFaceCreateDto assetFaceCreateDto,) async { + Future createFaceWithHttpInfo(AssetFaceCreateDto assetFaceCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces'; @@ -47,6 +47,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class FacesApi { /// Parameters: /// /// * [AssetFaceCreateDto] assetFaceCreateDto (required): - Future createFace(AssetFaceCreateDto assetFaceCreateDto,) async { - final response = await createFaceWithHttpInfo(assetFaceCreateDto,); + Future createFace(AssetFaceCreateDto assetFaceCreateDto, { Future? abortTrigger, }) async { + final response = await createFaceWithHttpInfo(assetFaceCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -75,7 +76,7 @@ class FacesApi { /// * [String] id (required): /// /// * [AssetFaceDeleteDto] assetFaceDeleteDto (required): - Future deleteFaceWithHttpInfo(String id, AssetFaceDeleteDto assetFaceDeleteDto,) async { + Future deleteFaceWithHttpInfo(String id, AssetFaceDeleteDto assetFaceDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces/{id}' .replaceAll('{id}', id); @@ -98,6 +99,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -110,8 +112,8 @@ class FacesApi { /// * [String] id (required): /// /// * [AssetFaceDeleteDto] assetFaceDeleteDto (required): - Future deleteFace(String id, AssetFaceDeleteDto assetFaceDeleteDto,) async { - final response = await deleteFaceWithHttpInfo(id, assetFaceDeleteDto,); + Future deleteFace(String id, AssetFaceDeleteDto assetFaceDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteFaceWithHttpInfo(id, assetFaceDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -127,7 +129,7 @@ class FacesApi { /// /// * [String] id (required): /// Face ID - Future getFacesWithHttpInfo(String id,) async { + Future getFacesWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces'; @@ -151,6 +153,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -162,8 +165,8 @@ class FacesApi { /// /// * [String] id (required): /// Face ID - Future?> getFaces(String id,) async { - final response = await getFacesWithHttpInfo(id,); + Future?> getFaces(String id, { Future? abortTrigger, }) async { + final response = await getFacesWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -191,7 +194,7 @@ class FacesApi { /// * [String] id (required): /// /// * [FaceDto] faceDto (required): - Future reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto,) async { + Future reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/faces/{id}' .replaceAll('{id}', id); @@ -214,6 +217,7 @@ class FacesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -226,8 +230,8 @@ class FacesApi { /// * [String] id (required): /// /// * [FaceDto] faceDto (required): - Future reassignFacesById(String id, FaceDto faceDto,) async { - final response = await reassignFacesByIdWithHttpInfo(id, faceDto,); + Future reassignFacesById(String id, FaceDto faceDto, { Future? abortTrigger, }) async { + final response = await reassignFacesByIdWithHttpInfo(id, faceDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 9dda59a883..287432ad9a 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -25,7 +25,7 @@ class JobsApi { /// Parameters: /// /// * [JobCreateDto] jobCreateDto (required): - Future createJobWithHttpInfo(JobCreateDto jobCreateDto,) async { + Future createJobWithHttpInfo(JobCreateDto jobCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs'; @@ -47,6 +47,7 @@ class JobsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class JobsApi { /// Parameters: /// /// * [JobCreateDto] jobCreateDto (required): - Future createJob(JobCreateDto jobCreateDto,) async { - final response = await createJobWithHttpInfo(jobCreateDto,); + Future createJob(JobCreateDto jobCreateDto, { Future? abortTrigger, }) async { + final response = await createJobWithHttpInfo(jobCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class JobsApi { /// Retrieve the counts of the current queue, as well as the current status. /// /// Note: This method returns the HTTP [Response]. - Future getQueuesLegacyWithHttpInfo() async { + Future getQueuesLegacyWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs'; @@ -91,14 +92,15 @@ class JobsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. - Future getQueuesLegacy() async { - final response = await getQueuesLegacyWithHttpInfo(); + Future getQueuesLegacy({ Future? abortTrigger, }) async { + final response = await getQueuesLegacyWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -123,7 +125,7 @@ class JobsApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { + Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/jobs/{name}' .replaceAll('{name}', name.toString()); @@ -146,6 +148,7 @@ class JobsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -158,8 +161,8 @@ class JobsApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { - final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); + Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto, { Future? abortTrigger, }) async { + final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/libraries_api.dart b/mobile/openapi/lib/api/libraries_api.dart index ca59f823fe..a3b3086994 100644 --- a/mobile/openapi/lib/api/libraries_api.dart +++ b/mobile/openapi/lib/api/libraries_api.dart @@ -25,7 +25,7 @@ class LibrariesApi { /// Parameters: /// /// * [CreateLibraryDto] createLibraryDto (required): - Future createLibraryWithHttpInfo(CreateLibraryDto createLibraryDto,) async { + Future createLibraryWithHttpInfo(CreateLibraryDto createLibraryDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries'; @@ -47,6 +47,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class LibrariesApi { /// Parameters: /// /// * [CreateLibraryDto] createLibraryDto (required): - Future createLibrary(CreateLibraryDto createLibraryDto,) async { - final response = await createLibraryWithHttpInfo(createLibraryDto,); + Future createLibrary(CreateLibraryDto createLibraryDto, { Future? abortTrigger, }) async { + final response = await createLibraryWithHttpInfo(createLibraryDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future deleteLibraryWithHttpInfo(String id,) async { + Future deleteLibraryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future deleteLibrary(String id,) async { - final response = await deleteLibraryWithHttpInfo(id,); + Future deleteLibrary(String id, { Future? abortTrigger, }) async { + final response = await deleteLibraryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -126,7 +128,7 @@ class LibrariesApi { /// Retrieve a list of external libraries. /// /// Note: This method returns the HTTP [Response]. - Future getAllLibrariesWithHttpInfo() async { + Future getAllLibrariesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries'; @@ -148,14 +150,15 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve libraries /// /// Retrieve a list of external libraries. - Future?> getAllLibraries() async { - final response = await getAllLibrariesWithHttpInfo(); + Future?> getAllLibraries({ Future? abortTrigger, }) async { + final response = await getAllLibrariesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -181,7 +184,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibraryWithHttpInfo(String id,) async { + Future getLibraryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}' .replaceAll('{id}', id); @@ -204,6 +207,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -214,8 +218,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibrary(String id,) async { - final response = await getLibraryWithHttpInfo(id,); + Future getLibrary(String id, { Future? abortTrigger, }) async { + final response = await getLibraryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -238,7 +242,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibraryStatisticsWithHttpInfo(String id,) async { + Future getLibraryStatisticsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}/statistics' .replaceAll('{id}', id); @@ -261,6 +265,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -271,8 +276,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibraryStatistics(String id,) async { - final response = await getLibraryStatisticsWithHttpInfo(id,); + Future getLibraryStatistics(String id, { Future? abortTrigger, }) async { + final response = await getLibraryStatisticsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -295,7 +300,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future scanLibraryWithHttpInfo(String id,) async { + Future scanLibraryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}/scan' .replaceAll('{id}', id); @@ -318,6 +323,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -328,8 +334,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future scanLibrary(String id,) async { - final response = await scanLibraryWithHttpInfo(id,); + Future scanLibrary(String id, { Future? abortTrigger, }) async { + final response = await scanLibraryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -346,7 +352,7 @@ class LibrariesApi { /// * [String] id (required): /// /// * [UpdateLibraryDto] updateLibraryDto (required): - Future updateLibraryWithHttpInfo(String id, UpdateLibraryDto updateLibraryDto,) async { + Future updateLibraryWithHttpInfo(String id, UpdateLibraryDto updateLibraryDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}' .replaceAll('{id}', id); @@ -369,6 +375,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -381,8 +388,8 @@ class LibrariesApi { /// * [String] id (required): /// /// * [UpdateLibraryDto] updateLibraryDto (required): - Future updateLibrary(String id, UpdateLibraryDto updateLibraryDto,) async { - final response = await updateLibraryWithHttpInfo(id, updateLibraryDto,); + Future updateLibrary(String id, UpdateLibraryDto updateLibraryDto, { Future? abortTrigger, }) async { + final response = await updateLibraryWithHttpInfo(id, updateLibraryDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -407,7 +414,7 @@ class LibrariesApi { /// * [String] id (required): /// /// * [ValidateLibraryDto] validateLibraryDto (required): - Future validateWithHttpInfo(String id, ValidateLibraryDto validateLibraryDto,) async { + Future validateWithHttpInfo(String id, ValidateLibraryDto validateLibraryDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/libraries/{id}/validate' .replaceAll('{id}', id); @@ -430,6 +437,7 @@ class LibrariesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -442,8 +450,8 @@ class LibrariesApi { /// * [String] id (required): /// /// * [ValidateLibraryDto] validateLibraryDto (required): - Future validate(String id, ValidateLibraryDto validateLibraryDto,) async { - final response = await validateWithHttpInfo(id, validateLibraryDto,); + Future validate(String id, ValidateLibraryDto validateLibraryDto, { Future? abortTrigger, }) async { + final response = await validateWithHttpInfo(id, validateLibraryDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/maintenance_admin_api.dart b/mobile/openapi/lib/api/maintenance_admin_api.dart index 0f953f1634..8bab193ddf 100644 --- a/mobile/openapi/lib/api/maintenance_admin_api.dart +++ b/mobile/openapi/lib/api/maintenance_admin_api.dart @@ -21,7 +21,7 @@ class MaintenanceAdminApi { /// Collect integrity checks and other heuristics about local data. /// /// Note: This method returns the HTTP [Response]. - Future detectPriorInstallWithHttpInfo() async { + Future detectPriorInstallWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance/detect-install'; @@ -43,14 +43,15 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Detect existing install /// /// Collect integrity checks and other heuristics about local data. - Future detectPriorInstall() async { - final response = await detectPriorInstallWithHttpInfo(); + Future detectPriorInstall({ Future? abortTrigger, }) async { + final response = await detectPriorInstallWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class MaintenanceAdminApi { /// Fetch information about the currently running maintenance action. /// /// Note: This method returns the HTTP [Response]. - Future getMaintenanceStatusWithHttpInfo() async { + Future getMaintenanceStatusWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance/status'; @@ -91,14 +92,15 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get maintenance mode status /// /// Fetch information about the currently running maintenance action. - Future getMaintenanceStatus() async { - final response = await getMaintenanceStatusWithHttpInfo(); + Future getMaintenanceStatus({ Future? abortTrigger, }) async { + final response = await getMaintenanceStatusWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -121,7 +123,7 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [MaintenanceLoginDto] maintenanceLoginDto (required): - Future maintenanceLoginWithHttpInfo(MaintenanceLoginDto maintenanceLoginDto,) async { + Future maintenanceLoginWithHttpInfo(MaintenanceLoginDto maintenanceLoginDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance/login'; @@ -143,6 +145,7 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -153,8 +156,8 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [MaintenanceLoginDto] maintenanceLoginDto (required): - Future maintenanceLogin(MaintenanceLoginDto maintenanceLoginDto,) async { - final response = await maintenanceLoginWithHttpInfo(maintenanceLoginDto,); + Future maintenanceLogin(MaintenanceLoginDto maintenanceLoginDto, { Future? abortTrigger, }) async { + final response = await maintenanceLoginWithHttpInfo(maintenanceLoginDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -177,7 +180,7 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [SetMaintenanceModeDto] setMaintenanceModeDto (required): - Future setMaintenanceModeWithHttpInfo(SetMaintenanceModeDto setMaintenanceModeDto,) async { + Future setMaintenanceModeWithHttpInfo(SetMaintenanceModeDto setMaintenanceModeDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/maintenance'; @@ -199,6 +202,7 @@ class MaintenanceAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -209,8 +213,8 @@ class MaintenanceAdminApi { /// Parameters: /// /// * [SetMaintenanceModeDto] setMaintenanceModeDto (required): - Future setMaintenanceMode(SetMaintenanceModeDto setMaintenanceModeDto,) async { - final response = await setMaintenanceModeWithHttpInfo(setMaintenanceModeDto,); + Future setMaintenanceMode(SetMaintenanceModeDto setMaintenanceModeDto, { Future? abortTrigger, }) async { + final response = await setMaintenanceModeWithHttpInfo(setMaintenanceModeDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/map_api.dart b/mobile/openapi/lib/api/map_api.dart index 4ce62bd96c..7e1618a875 100644 --- a/mobile/openapi/lib/api/map_api.dart +++ b/mobile/openapi/lib/api/map_api.dart @@ -41,7 +41,7 @@ class MapApi { /// /// * [bool] withSharedAlbums: /// Include shared album assets - Future getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { + Future getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/map/markers'; @@ -82,6 +82,7 @@ class MapApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -108,8 +109,8 @@ class MapApi { /// /// * [bool] withSharedAlbums: /// Include shared album assets - Future?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { - final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, ); + Future?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, Future? abortTrigger, }) async { + final response = await getMapMarkersWithHttpInfo(fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -139,7 +140,7 @@ class MapApi { /// /// * [double] lon (required): /// Longitude (-180 to 180) - Future reverseGeocodeWithHttpInfo(double lat, double lon,) async { + Future reverseGeocodeWithHttpInfo(double lat, double lon, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/map/reverse-geocode'; @@ -164,6 +165,7 @@ class MapApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -178,8 +180,8 @@ class MapApi { /// /// * [double] lon (required): /// Longitude (-180 to 180) - Future?> reverseGeocode(double lat, double lon,) async { - final response = await reverseGeocodeWithHttpInfo(lat, lon,); + Future?> reverseGeocode(double lat, double lon, { Future? abortTrigger, }) async { + final response = await reverseGeocodeWithHttpInfo(lat, lon, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 0cd96ac442..f5c653765a 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -27,7 +27,7 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future addMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future addMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}/assets' .replaceAll('{id}', id); @@ -50,6 +50,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> addMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await addMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> addMemoryAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await addMemoryAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class MemoriesApi { /// Parameters: /// /// * [MemoryCreateDto] memoryCreateDto (required): - Future createMemoryWithHttpInfo(MemoryCreateDto memoryCreateDto,) async { + Future createMemoryWithHttpInfo(MemoryCreateDto memoryCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -111,6 +112,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -121,8 +123,8 @@ class MemoriesApi { /// Parameters: /// /// * [MemoryCreateDto] memoryCreateDto (required): - Future createMemory(MemoryCreateDto memoryCreateDto,) async { - final response = await createMemoryWithHttpInfo(memoryCreateDto,); + Future createMemory(MemoryCreateDto memoryCreateDto, { Future? abortTrigger, }) async { + final response = await createMemoryWithHttpInfo(memoryCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -145,7 +147,7 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future deleteMemoryWithHttpInfo(String id,) async { + Future deleteMemoryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}' .replaceAll('{id}', id); @@ -168,6 +170,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -178,8 +181,8 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future deleteMemory(String id,) async { - final response = await deleteMemoryWithHttpInfo(id,); + Future deleteMemory(String id, { Future? abortTrigger, }) async { + final response = await deleteMemoryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -194,7 +197,7 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future getMemoryWithHttpInfo(String id,) async { + Future getMemoryWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}' .replaceAll('{id}', id); @@ -217,6 +220,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -227,8 +231,8 @@ class MemoriesApi { /// Parameters: /// /// * [String] id (required): - Future getMemory(String id,) async { - final response = await getMemoryWithHttpInfo(id,); + Future getMemory(String id, { Future? abortTrigger, }) async { + final response = await getMemoryWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -265,7 +269,7 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { + Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -306,6 +310,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -330,8 +335,8 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { - final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); + Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { + final response = await memoriesStatisticsWithHttpInfo(for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -356,7 +361,7 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future removeMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future removeMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}/assets' .replaceAll('{id}', id); @@ -379,6 +384,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -391,8 +397,8 @@ class MemoriesApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await removeMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await removeMemoryAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -432,7 +438,7 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { + Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -473,6 +479,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -497,8 +504,8 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { - final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); + Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, Future? abortTrigger, }) async { + final response = await searchMemoriesWithHttpInfo(for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -526,7 +533,7 @@ class MemoriesApi { /// * [String] id (required): /// /// * [MemoryUpdateDto] memoryUpdateDto (required): - Future updateMemoryWithHttpInfo(String id, MemoryUpdateDto memoryUpdateDto,) async { + Future updateMemoryWithHttpInfo(String id, MemoryUpdateDto memoryUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/{id}' .replaceAll('{id}', id); @@ -549,6 +556,7 @@ class MemoriesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -561,8 +569,8 @@ class MemoriesApi { /// * [String] id (required): /// /// * [MemoryUpdateDto] memoryUpdateDto (required): - Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto,) async { - final response = await updateMemoryWithHttpInfo(id, memoryUpdateDto,); + Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto, { Future? abortTrigger, }) async { + final response = await updateMemoryWithHttpInfo(id, memoryUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/notifications_admin_api.dart b/mobile/openapi/lib/api/notifications_admin_api.dart index 7821553d30..e9e18e791e 100644 --- a/mobile/openapi/lib/api/notifications_admin_api.dart +++ b/mobile/openapi/lib/api/notifications_admin_api.dart @@ -25,7 +25,7 @@ class NotificationsAdminApi { /// Parameters: /// /// * [NotificationCreateDto] notificationCreateDto (required): - Future createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto,) async { + Future createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/notifications'; @@ -47,6 +47,7 @@ class NotificationsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class NotificationsAdminApi { /// Parameters: /// /// * [NotificationCreateDto] notificationCreateDto (required): - Future createNotification(NotificationCreateDto notificationCreateDto,) async { - final response = await createNotificationWithHttpInfo(notificationCreateDto,); + Future createNotification(NotificationCreateDto notificationCreateDto, { Future? abortTrigger, }) async { + final response = await createNotificationWithHttpInfo(notificationCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -83,7 +84,7 @@ class NotificationsAdminApi { /// * [String] name (required): /// /// * [TemplateDto] templateDto (required): - Future getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async { + Future getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/notifications/templates/{name}' .replaceAll('{name}', name); @@ -106,6 +107,7 @@ class NotificationsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -118,8 +120,8 @@ class NotificationsAdminApi { /// * [String] name (required): /// /// * [TemplateDto] templateDto (required): - Future getNotificationTemplateAdmin(String name, TemplateDto templateDto,) async { - final response = await getNotificationTemplateAdminWithHttpInfo(name, templateDto,); + Future getNotificationTemplateAdmin(String name, TemplateDto templateDto, { Future? abortTrigger, }) async { + final response = await getNotificationTemplateAdminWithHttpInfo(name, templateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -142,7 +144,7 @@ class NotificationsAdminApi { /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): - Future sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async { + Future sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/notifications/test-email'; @@ -164,6 +166,7 @@ class NotificationsAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -174,8 +177,8 @@ class NotificationsAdminApi { /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): - Future sendTestEmailAdmin(SystemConfigSmtpDto systemConfigSmtpDto,) async { - final response = await sendTestEmailAdminWithHttpInfo(systemConfigSmtpDto,); + Future sendTestEmailAdmin(SystemConfigSmtpDto systemConfigSmtpDto, { Future? abortTrigger, }) async { + final response = await sendTestEmailAdminWithHttpInfo(systemConfigSmtpDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index ab0be3e8f3..6b4f213bcd 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -25,7 +25,7 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future deleteNotificationWithHttpInfo(String id,) async { + Future deleteNotificationWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future deleteNotification(String id,) async { - final response = await deleteNotificationWithHttpInfo(id,); + Future deleteNotification(String id, { Future? abortTrigger, }) async { + final response = await deleteNotificationWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -74,7 +75,7 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationDeleteAllDto] notificationDeleteAllDto (required): - Future deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto,) async { + Future deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications'; @@ -96,6 +97,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -106,8 +108,8 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationDeleteAllDto] notificationDeleteAllDto (required): - Future deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto,) async { - final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto,); + Future deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto, { Future? abortTrigger, }) async { + final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -122,7 +124,7 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future getNotificationWithHttpInfo(String id,) async { + Future getNotificationWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications/{id}' .replaceAll('{id}', id); @@ -145,6 +147,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -155,8 +158,8 @@ class NotificationsApi { /// Parameters: /// /// * [String] id (required): - Future getNotification(String id,) async { - final response = await getNotificationWithHttpInfo(id,); + Future getNotification(String id, { Future? abortTrigger, }) async { + final response = await getNotificationWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -187,7 +190,7 @@ class NotificationsApi { /// /// * [bool] unread: /// Filter by unread status - Future getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { + Future getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications'; @@ -222,6 +225,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -240,8 +244,8 @@ class NotificationsApi { /// /// * [bool] unread: /// Filter by unread status - Future?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { - final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, ); + Future?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, Future? abortTrigger, }) async { + final response = await getNotificationsWithHttpInfo(id: id, level: level, type: type, unread: unread, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -269,7 +273,7 @@ class NotificationsApi { /// * [String] id (required): /// /// * [NotificationUpdateDto] notificationUpdateDto (required): - Future updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto,) async { + Future updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications/{id}' .replaceAll('{id}', id); @@ -292,6 +296,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -304,8 +309,8 @@ class NotificationsApi { /// * [String] id (required): /// /// * [NotificationUpdateDto] notificationUpdateDto (required): - Future updateNotification(String id, NotificationUpdateDto notificationUpdateDto,) async { - final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto,); + Future updateNotification(String id, NotificationUpdateDto notificationUpdateDto, { Future? abortTrigger, }) async { + final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -328,7 +333,7 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationUpdateAllDto] notificationUpdateAllDto (required): - Future updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto,) async { + Future updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications'; @@ -350,6 +355,7 @@ class NotificationsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -360,8 +366,8 @@ class NotificationsApi { /// Parameters: /// /// * [NotificationUpdateAllDto] notificationUpdateAllDto (required): - Future updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto,) async { - final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto,); + Future updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto, { Future? abortTrigger, }) async { + final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index 7d18f6d867..45bcdcd085 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -25,7 +25,7 @@ class PartnersApi { /// Parameters: /// /// * [PartnerCreateDto] partnerCreateDto (required): - Future createPartnerWithHttpInfo(PartnerCreateDto partnerCreateDto,) async { + Future createPartnerWithHttpInfo(PartnerCreateDto partnerCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -47,6 +47,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class PartnersApi { /// Parameters: /// /// * [PartnerCreateDto] partnerCreateDto (required): - Future createPartner(PartnerCreateDto partnerCreateDto,) async { - final response = await createPartnerWithHttpInfo(partnerCreateDto,); + Future createPartner(PartnerCreateDto partnerCreateDto, { Future? abortTrigger, }) async { + final response = await createPartnerWithHttpInfo(partnerCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecatedWithHttpInfo(String id,) async { + Future createPartnerDeprecatedWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future createPartnerDeprecated(String id,) async { - final response = await createPartnerDeprecatedWithHttpInfo(id,); + Future createPartnerDeprecated(String id, { Future? abortTrigger, }) async { + final response = await createPartnerDeprecatedWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -138,7 +140,7 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - Future getPartnersWithHttpInfo(PartnerDirection direction,) async { + Future getPartnersWithHttpInfo(PartnerDirection direction, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -162,6 +164,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -172,8 +175,8 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - Future?> getPartners(PartnerDirection direction,) async { - final response = await getPartnersWithHttpInfo(direction,); + Future?> getPartners(PartnerDirection direction, { Future? abortTrigger, }) async { + final response = await getPartnersWithHttpInfo(direction, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -199,7 +202,7 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future removePartnerWithHttpInfo(String id,) async { + Future removePartnerWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -222,6 +225,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -232,8 +236,8 @@ class PartnersApi { /// Parameters: /// /// * [String] id (required): - Future removePartner(String id,) async { - final response = await removePartnerWithHttpInfo(id,); + Future removePartner(String id, { Future? abortTrigger, }) async { + final response = await removePartnerWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -250,7 +254,7 @@ class PartnersApi { /// * [String] id (required): /// /// * [PartnerUpdateDto] partnerUpdateDto (required): - Future updatePartnerWithHttpInfo(String id, PartnerUpdateDto partnerUpdateDto,) async { + Future updatePartnerWithHttpInfo(String id, PartnerUpdateDto partnerUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/partners/{id}' .replaceAll('{id}', id); @@ -273,6 +277,7 @@ class PartnersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -285,8 +290,8 @@ class PartnersApi { /// * [String] id (required): /// /// * [PartnerUpdateDto] partnerUpdateDto (required): - Future updatePartner(String id, PartnerUpdateDto partnerUpdateDto,) async { - final response = await updatePartnerWithHttpInfo(id, partnerUpdateDto,); + Future updatePartner(String id, PartnerUpdateDto partnerUpdateDto, { Future? abortTrigger, }) async { + final response = await updatePartnerWithHttpInfo(id, partnerUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 99821f31aa..c35491e110 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -25,7 +25,7 @@ class PeopleApi { /// Parameters: /// /// * [PersonCreateDto] personCreateDto (required): - Future createPersonWithHttpInfo(PersonCreateDto personCreateDto,) async { + Future createPersonWithHttpInfo(PersonCreateDto personCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -47,6 +47,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class PeopleApi { /// Parameters: /// /// * [PersonCreateDto] personCreateDto (required): - Future createPerson(PersonCreateDto personCreateDto,) async { - final response = await createPersonWithHttpInfo(personCreateDto,); + Future createPerson(PersonCreateDto personCreateDto, { Future? abortTrigger, }) async { + final response = await createPersonWithHttpInfo(personCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class PeopleApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deletePeopleWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future deletePeopleWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -103,6 +104,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class PeopleApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deletePeople(BulkIdsDto bulkIdsDto,) async { - final response = await deletePeopleWithHttpInfo(bulkIdsDto,); + Future deletePeople(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await deletePeopleWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -129,7 +131,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future deletePersonWithHttpInfo(String id,) async { + Future deletePersonWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}' .replaceAll('{id}', id); @@ -152,6 +154,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -162,8 +165,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future deletePerson(String id,) async { - final response = await deletePersonWithHttpInfo(id,); + Future deletePerson(String id, { Future? abortTrigger, }) async { + final response = await deletePersonWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -191,7 +194,7 @@ class PeopleApi { /// /// * [bool] withHidden: /// Include hidden people - Future getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { + Future getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -229,6 +232,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -252,8 +256,8 @@ class PeopleApi { /// /// * [bool] withHidden: /// Include hidden people - Future getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { - final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, ); + Future getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, Future? abortTrigger, }) async { + final response = await getAllPeopleWithHttpInfo(closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -276,7 +280,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonWithHttpInfo(String id,) async { + Future getPersonWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}' .replaceAll('{id}', id); @@ -299,6 +303,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -309,8 +314,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPerson(String id,) async { - final response = await getPersonWithHttpInfo(id,); + Future getPerson(String id, { Future? abortTrigger, }) async { + final response = await getPersonWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -333,7 +338,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonStatisticsWithHttpInfo(String id,) async { + Future getPersonStatisticsWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/statistics' .replaceAll('{id}', id); @@ -356,6 +361,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -366,8 +372,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonStatistics(String id,) async { - final response = await getPersonStatisticsWithHttpInfo(id,); + Future getPersonStatistics(String id, { Future? abortTrigger, }) async { + final response = await getPersonStatisticsWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -390,7 +396,7 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonThumbnailWithHttpInfo(String id,) async { + Future getPersonThumbnailWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/thumbnail' .replaceAll('{id}', id); @@ -413,6 +419,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -423,8 +430,8 @@ class PeopleApi { /// Parameters: /// /// * [String] id (required): - Future getPersonThumbnail(String id,) async { - final response = await getPersonThumbnailWithHttpInfo(id,); + Future getPersonThumbnail(String id, { Future? abortTrigger, }) async { + final response = await getPersonThumbnailWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -449,7 +456,7 @@ class PeopleApi { /// * [String] id (required): /// /// * [MergePersonDto] mergePersonDto (required): - Future mergePersonWithHttpInfo(String id, MergePersonDto mergePersonDto,) async { + Future mergePersonWithHttpInfo(String id, MergePersonDto mergePersonDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/merge' .replaceAll('{id}', id); @@ -472,6 +479,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -484,8 +492,8 @@ class PeopleApi { /// * [String] id (required): /// /// * [MergePersonDto] mergePersonDto (required): - Future?> mergePerson(String id, MergePersonDto mergePersonDto,) async { - final response = await mergePersonWithHttpInfo(id, mergePersonDto,); + Future?> mergePerson(String id, MergePersonDto mergePersonDto, { Future? abortTrigger, }) async { + final response = await mergePersonWithHttpInfo(id, mergePersonDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -513,7 +521,7 @@ class PeopleApi { /// * [String] id (required): /// /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): - Future reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async { + Future reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}/reassign' .replaceAll('{id}', id); @@ -536,6 +544,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -548,8 +557,8 @@ class PeopleApi { /// * [String] id (required): /// /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): - Future?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async { - final response = await reassignFacesWithHttpInfo(id, assetFaceUpdateDto,); + Future?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto, { Future? abortTrigger, }) async { + final response = await reassignFacesWithHttpInfo(id, assetFaceUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -575,7 +584,7 @@ class PeopleApi { /// Parameters: /// /// * [PeopleUpdateDto] peopleUpdateDto (required): - Future updatePeopleWithHttpInfo(PeopleUpdateDto peopleUpdateDto,) async { + Future updatePeopleWithHttpInfo(PeopleUpdateDto peopleUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -597,6 +606,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -607,8 +617,8 @@ class PeopleApi { /// Parameters: /// /// * [PeopleUpdateDto] peopleUpdateDto (required): - Future?> updatePeople(PeopleUpdateDto peopleUpdateDto,) async { - final response = await updatePeopleWithHttpInfo(peopleUpdateDto,); + Future?> updatePeople(PeopleUpdateDto peopleUpdateDto, { Future? abortTrigger, }) async { + final response = await updatePeopleWithHttpInfo(peopleUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -636,7 +646,7 @@ class PeopleApi { /// * [String] id (required): /// /// * [PersonUpdateDto] personUpdateDto (required): - Future updatePersonWithHttpInfo(String id, PersonUpdateDto personUpdateDto,) async { + Future updatePersonWithHttpInfo(String id, PersonUpdateDto personUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/people/{id}' .replaceAll('{id}', id); @@ -659,6 +669,7 @@ class PeopleApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -671,8 +682,8 @@ class PeopleApi { /// * [String] id (required): /// /// * [PersonUpdateDto] personUpdateDto (required): - Future updatePerson(String id, PersonUpdateDto personUpdateDto,) async { - final response = await updatePersonWithHttpInfo(id, personUpdateDto,); + Future updatePerson(String id, PersonUpdateDto personUpdateDto, { Future? abortTrigger, }) async { + final response = await updatePersonWithHttpInfo(id, personUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/plugins_api.dart b/mobile/openapi/lib/api/plugins_api.dart index a29f597dc4..40892b8a67 100644 --- a/mobile/openapi/lib/api/plugins_api.dart +++ b/mobile/openapi/lib/api/plugins_api.dart @@ -25,7 +25,7 @@ class PluginsApi { /// Parameters: /// /// * [String] id (required): - Future getPluginWithHttpInfo(String id,) async { + Future getPluginWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins/{id}' .replaceAll('{id}', id); @@ -48,6 +48,7 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -58,8 +59,8 @@ class PluginsApi { /// Parameters: /// /// * [String] id (required): - Future getPlugin(String id,) async { - final response = await getPluginWithHttpInfo(id,); + Future getPlugin(String id, { Future? abortTrigger, }) async { + final response = await getPluginWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -104,7 +105,7 @@ class PluginsApi { /// /// * [WorkflowType] type: /// Workflow types - Future searchPluginMethodsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async { + Future searchPluginMethodsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins/methods'; @@ -154,6 +155,7 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -186,8 +188,8 @@ class PluginsApi { /// /// * [WorkflowType] type: /// Workflow types - Future?> searchPluginMethods({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async { - final response = await searchPluginMethodsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, pluginName: pluginName, pluginVersion: pluginVersion, title: title, trigger: trigger, type: type, ); + Future?> searchPluginMethods({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, Future? abortTrigger, }) async { + final response = await searchPluginMethodsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, pluginName: pluginName, pluginVersion: pluginVersion, title: title, trigger: trigger, type: type, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -209,7 +211,7 @@ class PluginsApi { /// Retrieve workflow templates provided by installed plugins /// /// Note: This method returns the HTTP [Response]. - Future searchPluginTemplatesWithHttpInfo() async { + Future searchPluginTemplatesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins/templates'; @@ -231,14 +233,15 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve workflow templates /// /// Retrieve workflow templates provided by installed plugins - Future?> searchPluginTemplates() async { - final response = await searchPluginTemplatesWithHttpInfo(); + Future?> searchPluginTemplates({ Future? abortTrigger, }) async { + final response = await searchPluginTemplatesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -276,7 +279,7 @@ class PluginsApi { /// * [String] title: /// /// * [String] version: - Future searchPluginsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async { + Future searchPluginsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? title, String? version, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/plugins'; @@ -317,6 +320,7 @@ class PluginsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -339,8 +343,8 @@ class PluginsApi { /// * [String] title: /// /// * [String] version: - Future?> searchPlugins({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async { - final response = await searchPluginsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, title: title, version: version, ); + Future?> searchPlugins({ String? description, bool? enabled, String? id, String? name, String? title, String? version, Future? abortTrigger, }) async { + final response = await searchPluginsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, title: title, version: version, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart index 1312cb5952..39386c23f9 100644 --- a/mobile/openapi/lib/api/queues_api.dart +++ b/mobile/openapi/lib/api/queues_api.dart @@ -27,7 +27,7 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueDeleteDto] queueDeleteDto (required): - Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { + Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}/jobs' .replaceAll('{name}', name.toString()); @@ -50,6 +50,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueDeleteDto] queueDeleteDto (required): - Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { - final response = await emptyQueueWithHttpInfo(name, queueDeleteDto,); + Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto, { Future? abortTrigger, }) async { + final response = await emptyQueueWithHttpInfo(name, queueDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -78,7 +79,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - Future getQueueWithHttpInfo(QueueName name,) async { + Future getQueueWithHttpInfo(QueueName name, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' .replaceAll('{name}', name.toString()); @@ -101,6 +102,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -111,8 +113,8 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - Future getQueue(QueueName name,) async { - final response = await getQueueWithHttpInfo(name,); + Future getQueue(QueueName name, { Future? abortTrigger, }) async { + final response = await getQueueWithHttpInfo(name, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -138,7 +140,7 @@ class QueuesApi { /// /// * [List] status: /// Filter jobs by status - Future getQueueJobsWithHttpInfo(QueueName name, { List? status, }) async { + Future getQueueJobsWithHttpInfo(QueueName name, { List? status, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}/jobs' .replaceAll('{name}', name.toString()); @@ -165,6 +167,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -178,8 +181,8 @@ class QueuesApi { /// /// * [List] status: /// Filter jobs by status - Future?> getQueueJobs(QueueName name, { List? status, }) async { - final response = await getQueueJobsWithHttpInfo(name, status: status, ); + Future?> getQueueJobs(QueueName name, { List? status, Future? abortTrigger, }) async { + final response = await getQueueJobsWithHttpInfo(name, status: status, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -201,7 +204,7 @@ class QueuesApi { /// Retrieves a list of queues. /// /// Note: This method returns the HTTP [Response]. - Future getQueuesWithHttpInfo() async { + Future getQueuesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues'; @@ -223,14 +226,15 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List all queues /// /// Retrieves a list of queues. - Future?> getQueues() async { - final response = await getQueuesWithHttpInfo(); + Future?> getQueues({ Future? abortTrigger, }) async { + final response = await getQueuesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -258,7 +262,7 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueUpdateDto] queueUpdateDto (required): - Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { + Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' .replaceAll('{name}', name.toString()); @@ -281,6 +285,7 @@ class QueuesApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -293,8 +298,8 @@ class QueuesApi { /// * [QueueName] name (required): /// /// * [QueueUpdateDto] queueUpdateDto (required): - Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { - final response = await updateQueueWithHttpInfo(name, queueUpdateDto,); + Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto, { Future? abortTrigger, }) async { + final response = await updateQueueWithHttpInfo(name, queueUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 6f8a4df902..0118cabdba 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -21,7 +21,7 @@ class SearchApi { /// Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in. /// /// Note: This method returns the HTTP [Response]. - Future getAssetsByCityWithHttpInfo() async { + Future getAssetsByCityWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/cities'; @@ -43,14 +43,15 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve assets by city /// /// Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in. - Future?> getAssetsByCity() async { - final response = await getAssetsByCityWithHttpInfo(); + Future?> getAssetsByCity({ Future? abortTrigger, }) async { + final response = await getAssetsByCityWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -72,7 +73,7 @@ class SearchApi { /// Retrieve data for the explore section, such as popular people and places. /// /// Note: This method returns the HTTP [Response]. - Future getExploreDataWithHttpInfo() async { + Future getExploreDataWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/explore'; @@ -94,14 +95,15 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve explore data /// /// Retrieve data for the explore section, such as popular people and places. - Future?> getExploreData() async { - final response = await getExploreDataWithHttpInfo(); + Future?> getExploreData({ Future? abortTrigger, }) async { + final response = await getExploreDataWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -145,7 +147,7 @@ class SearchApi { /// /// * [String] state: /// Filter by state/province - Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { + Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/suggestions'; @@ -187,6 +189,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -215,8 +218,8 @@ class SearchApi { /// /// * [String] state: /// Filter by state/province - Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { - final response = await getSearchSuggestionsWithHttpInfo(type, country: country, includeNull: includeNull, lensModel: lensModel, make: make, model: model, state: state, ); + Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, Future? abortTrigger, }) async { + final response = await getSearchSuggestionsWithHttpInfo(type, country: country, includeNull: includeNull, lensModel: lensModel, make: make, model: model, state: state, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -242,7 +245,7 @@ class SearchApi { /// Parameters: /// /// * [StatisticsSearchDto] statisticsSearchDto (required): - Future searchAssetStatisticsWithHttpInfo(StatisticsSearchDto statisticsSearchDto,) async { + Future searchAssetStatisticsWithHttpInfo(StatisticsSearchDto statisticsSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/statistics'; @@ -264,6 +267,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -274,8 +278,8 @@ class SearchApi { /// Parameters: /// /// * [StatisticsSearchDto] statisticsSearchDto (required): - Future searchAssetStatistics(StatisticsSearchDto statisticsSearchDto,) async { - final response = await searchAssetStatisticsWithHttpInfo(statisticsSearchDto,); + Future searchAssetStatistics(StatisticsSearchDto statisticsSearchDto, { Future? abortTrigger, }) async { + final response = await searchAssetStatisticsWithHttpInfo(statisticsSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -298,7 +302,7 @@ class SearchApi { /// Parameters: /// /// * [MetadataSearchDto] metadataSearchDto (required): - Future searchAssetsWithHttpInfo(MetadataSearchDto metadataSearchDto,) async { + Future searchAssetsWithHttpInfo(MetadataSearchDto metadataSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/metadata'; @@ -320,6 +324,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -330,8 +335,8 @@ class SearchApi { /// Parameters: /// /// * [MetadataSearchDto] metadataSearchDto (required): - Future searchAssets(MetadataSearchDto metadataSearchDto,) async { - final response = await searchAssetsWithHttpInfo(metadataSearchDto,); + Future searchAssets(MetadataSearchDto metadataSearchDto, { Future? abortTrigger, }) async { + final response = await searchAssetsWithHttpInfo(metadataSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -443,7 +448,7 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { + Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/large-assets'; @@ -559,6 +564,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -658,8 +664,8 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { - final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); + Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { + final response = await searchLargeAssetsWithHttpInfo(albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -689,7 +695,7 @@ class SearchApi { /// /// * [bool] withHidden: /// Include hidden people - Future searchPersonWithHttpInfo(String name, { bool? withHidden, }) async { + Future searchPersonWithHttpInfo(String name, { bool? withHidden, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/person'; @@ -716,6 +722,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -730,8 +737,8 @@ class SearchApi { /// /// * [bool] withHidden: /// Include hidden people - Future?> searchPerson(String name, { bool? withHidden, }) async { - final response = await searchPersonWithHttpInfo(name, withHidden: withHidden, ); + Future?> searchPerson(String name, { bool? withHidden, Future? abortTrigger, }) async { + final response = await searchPersonWithHttpInfo(name, withHidden: withHidden, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -758,7 +765,7 @@ class SearchApi { /// /// * [String] name (required): /// Place name to search for - Future searchPlacesWithHttpInfo(String name,) async { + Future searchPlacesWithHttpInfo(String name, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/places'; @@ -782,6 +789,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -793,8 +801,8 @@ class SearchApi { /// /// * [String] name (required): /// Place name to search for - Future?> searchPlaces(String name,) async { - final response = await searchPlacesWithHttpInfo(name,); + Future?> searchPlaces(String name, { Future? abortTrigger, }) async { + final response = await searchPlacesWithHttpInfo(name, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -820,7 +828,7 @@ class SearchApi { /// Parameters: /// /// * [RandomSearchDto] randomSearchDto (required): - Future searchRandomWithHttpInfo(RandomSearchDto randomSearchDto,) async { + Future searchRandomWithHttpInfo(RandomSearchDto randomSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/random'; @@ -842,6 +850,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -852,8 +861,8 @@ class SearchApi { /// Parameters: /// /// * [RandomSearchDto] randomSearchDto (required): - Future?> searchRandom(RandomSearchDto randomSearchDto,) async { - final response = await searchRandomWithHttpInfo(randomSearchDto,); + Future?> searchRandom(RandomSearchDto randomSearchDto, { Future? abortTrigger, }) async { + final response = await searchRandomWithHttpInfo(randomSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -879,7 +888,7 @@ class SearchApi { /// Parameters: /// /// * [SmartSearchDto] smartSearchDto (required): - Future searchSmartWithHttpInfo(SmartSearchDto smartSearchDto,) async { + Future searchSmartWithHttpInfo(SmartSearchDto smartSearchDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/smart'; @@ -901,6 +910,7 @@ class SearchApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -911,8 +921,8 @@ class SearchApi { /// Parameters: /// /// * [SmartSearchDto] smartSearchDto (required): - Future searchSmart(SmartSearchDto smartSearchDto,) async { - final response = await searchSmartWithHttpInfo(smartSearchDto,); + Future searchSmart(SmartSearchDto smartSearchDto, { Future? abortTrigger, }) async { + final response = await searchSmartWithHttpInfo(smartSearchDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index dd38ade167..1a46a86188 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -21,7 +21,7 @@ class ServerApi { /// Delete the currently set server product key. /// /// Note: This method returns the HTTP [Response]. - Future deleteServerLicenseWithHttpInfo() async { + Future deleteServerLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/license'; @@ -43,14 +43,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete server product key /// /// Delete the currently set server product key. - Future deleteServerLicense() async { - final response = await deleteServerLicenseWithHttpInfo(); + Future deleteServerLicense({ Future? abortTrigger, }) async { + final response = await deleteServerLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -61,7 +62,7 @@ class ServerApi { /// Retrieve a list of information about the server. /// /// Note: This method returns the HTTP [Response]. - Future getAboutInfoWithHttpInfo() async { + Future getAboutInfoWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/about'; @@ -83,14 +84,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get server information /// /// Retrieve a list of information about the server. - Future getAboutInfo() async { - final response = await getAboutInfoWithHttpInfo(); + Future getAboutInfo({ Future? abortTrigger, }) async { + final response = await getAboutInfoWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -109,7 +111,7 @@ class ServerApi { /// Retrieve links to the APKs for the current server version. /// /// Note: This method returns the HTTP [Response]. - Future getApkLinksWithHttpInfo() async { + Future getApkLinksWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/apk-links'; @@ -131,14 +133,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get APK links /// /// Retrieve links to the APKs for the current server version. - Future getApkLinks() async { - final response = await getApkLinksWithHttpInfo(); + Future getApkLinks({ Future? abortTrigger, }) async { + final response = await getApkLinksWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -157,7 +160,7 @@ class ServerApi { /// Retrieve the current server configuration. /// /// Note: This method returns the HTTP [Response]. - Future getServerConfigWithHttpInfo() async { + Future getServerConfigWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/config'; @@ -179,14 +182,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get config /// /// Retrieve the current server configuration. - Future getServerConfig() async { - final response = await getServerConfigWithHttpInfo(); + Future getServerConfig({ Future? abortTrigger, }) async { + final response = await getServerConfigWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -205,7 +209,7 @@ class ServerApi { /// Retrieve available features supported by this server. /// /// Note: This method returns the HTTP [Response]. - Future getServerFeaturesWithHttpInfo() async { + Future getServerFeaturesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/features'; @@ -227,14 +231,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get features /// /// Retrieve available features supported by this server. - Future getServerFeatures() async { - final response = await getServerFeaturesWithHttpInfo(); + Future getServerFeatures({ Future? abortTrigger, }) async { + final response = await getServerFeaturesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -253,7 +258,7 @@ class ServerApi { /// Retrieve information about whether the server currently has a product key registered. /// /// Note: This method returns the HTTP [Response]. - Future getServerLicenseWithHttpInfo() async { + Future getServerLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/license'; @@ -275,14 +280,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get product key /// /// Retrieve information about whether the server currently has a product key registered. - Future getServerLicense() async { - final response = await getServerLicenseWithHttpInfo(); + Future getServerLicense({ Future? abortTrigger, }) async { + final response = await getServerLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -301,7 +307,7 @@ class ServerApi { /// Retrieve statistics about the entire Immich instance such as asset counts. /// /// Note: This method returns the HTTP [Response]. - Future getServerStatisticsWithHttpInfo() async { + Future getServerStatisticsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/statistics'; @@ -323,14 +329,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get statistics /// /// Retrieve statistics about the entire Immich instance such as asset counts. - Future getServerStatistics() async { - final response = await getServerStatisticsWithHttpInfo(); + Future getServerStatistics({ Future? abortTrigger, }) async { + final response = await getServerStatisticsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -349,7 +356,7 @@ class ServerApi { /// Retrieve the current server version in semantic versioning (semver) format. /// /// Note: This method returns the HTTP [Response]. - Future getServerVersionWithHttpInfo() async { + Future getServerVersionWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/version'; @@ -371,14 +378,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get server version /// /// Retrieve the current server version in semantic versioning (semver) format. - Future getServerVersion() async { - final response = await getServerVersionWithHttpInfo(); + Future getServerVersion({ Future? abortTrigger, }) async { + final response = await getServerVersionWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -397,7 +405,7 @@ class ServerApi { /// Retrieve the current storage utilization information of the server. /// /// Note: This method returns the HTTP [Response]. - Future getStorageWithHttpInfo() async { + Future getStorageWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/storage'; @@ -419,14 +427,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get storage /// /// Retrieve the current storage utilization information of the server. - Future getStorage() async { - final response = await getStorageWithHttpInfo(); + Future getStorage({ Future? abortTrigger, }) async { + final response = await getStorageWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -445,7 +454,7 @@ class ServerApi { /// Retrieve all media types supported by the server. /// /// Note: This method returns the HTTP [Response]. - Future getSupportedMediaTypesWithHttpInfo() async { + Future getSupportedMediaTypesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/media-types'; @@ -467,14 +476,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get supported media types /// /// Retrieve all media types supported by the server. - Future getSupportedMediaTypes() async { - final response = await getSupportedMediaTypesWithHttpInfo(); + Future getSupportedMediaTypes({ Future? abortTrigger, }) async { + final response = await getSupportedMediaTypesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -493,7 +503,7 @@ class ServerApi { /// Retrieve information about the last time the version check ran. /// /// Note: This method returns the HTTP [Response]. - Future getVersionCheckWithHttpInfo() async { + Future getVersionCheckWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/version-check'; @@ -515,14 +525,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get version check status /// /// Retrieve information about the last time the version check ran. - Future getVersionCheck() async { - final response = await getVersionCheckWithHttpInfo(); + Future getVersionCheck({ Future? abortTrigger, }) async { + final response = await getVersionCheckWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -541,7 +552,7 @@ class ServerApi { /// Retrieve a list of past versions the server has been on. /// /// Note: This method returns the HTTP [Response]. - Future getVersionHistoryWithHttpInfo() async { + Future getVersionHistoryWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/version-history'; @@ -563,14 +574,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get version history /// /// Retrieve a list of past versions the server has been on. - Future?> getVersionHistory() async { - final response = await getVersionHistoryWithHttpInfo(); + Future?> getVersionHistory({ Future? abortTrigger, }) async { + final response = await getVersionHistoryWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -592,7 +604,7 @@ class ServerApi { /// Pong /// /// Note: This method returns the HTTP [Response]. - Future pingServerWithHttpInfo() async { + Future pingServerWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/ping'; @@ -614,14 +626,15 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Ping /// /// Pong - Future pingServer() async { - final response = await pingServerWithHttpInfo(); + Future pingServer({ Future? abortTrigger, }) async { + final response = await pingServerWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -644,7 +657,7 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto,) async { + Future setServerLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/server/license'; @@ -666,6 +679,7 @@ class ServerApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -676,8 +690,8 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { - final response = await setServerLicenseWithHttpInfo(licenseKeyDto,); + Future setServerLicense(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { + final response = await setServerLicenseWithHttpInfo(licenseKeyDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart index da508059bc..fdd6c09266 100644 --- a/mobile/openapi/lib/api/sessions_api.dart +++ b/mobile/openapi/lib/api/sessions_api.dart @@ -25,7 +25,7 @@ class SessionsApi { /// Parameters: /// /// * [SessionCreateDto] sessionCreateDto (required): - Future createSessionWithHttpInfo(SessionCreateDto sessionCreateDto,) async { + Future createSessionWithHttpInfo(SessionCreateDto sessionCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions'; @@ -47,6 +47,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class SessionsApi { /// Parameters: /// /// * [SessionCreateDto] sessionCreateDto (required): - Future createSession(SessionCreateDto sessionCreateDto,) async { - final response = await createSessionWithHttpInfo(sessionCreateDto,); + Future createSession(SessionCreateDto sessionCreateDto, { Future? abortTrigger, }) async { + final response = await createSessionWithHttpInfo(sessionCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -77,7 +78,7 @@ class SessionsApi { /// Delete all sessions for the user. This will not delete the current session. /// /// Note: This method returns the HTTP [Response]. - Future deleteAllSessionsWithHttpInfo() async { + Future deleteAllSessionsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions'; @@ -99,14 +100,15 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete all sessions /// /// Delete all sessions for the user. This will not delete the current session. - Future deleteAllSessions() async { - final response = await deleteAllSessionsWithHttpInfo(); + Future deleteAllSessions({ Future? abortTrigger, }) async { + final response = await deleteAllSessionsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -121,7 +123,7 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future deleteSessionWithHttpInfo(String id,) async { + Future deleteSessionWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions/{id}' .replaceAll('{id}', id); @@ -144,6 +146,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -154,8 +157,8 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future deleteSession(String id,) async { - final response = await deleteSessionWithHttpInfo(id,); + Future deleteSession(String id, { Future? abortTrigger, }) async { + final response = await deleteSessionWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -166,7 +169,7 @@ class SessionsApi { /// Retrieve a list of sessions for the user. /// /// Note: This method returns the HTTP [Response]. - Future getSessionsWithHttpInfo() async { + Future getSessionsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions'; @@ -188,14 +191,15 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve sessions /// /// Retrieve a list of sessions for the user. - Future?> getSessions() async { - final response = await getSessionsWithHttpInfo(); + Future?> getSessions({ Future? abortTrigger, }) async { + final response = await getSessionsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -221,7 +225,7 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future lockSessionWithHttpInfo(String id,) async { + Future lockSessionWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions/{id}/lock' .replaceAll('{id}', id); @@ -244,6 +248,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -254,8 +259,8 @@ class SessionsApi { /// Parameters: /// /// * [String] id (required): - Future lockSession(String id,) async { - final response = await lockSessionWithHttpInfo(id,); + Future lockSession(String id, { Future? abortTrigger, }) async { + final response = await lockSessionWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -272,7 +277,7 @@ class SessionsApi { /// * [String] id (required): /// /// * [SessionUpdateDto] sessionUpdateDto (required): - Future updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto,) async { + Future updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sessions/{id}' .replaceAll('{id}', id); @@ -295,6 +300,7 @@ class SessionsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -307,8 +313,8 @@ class SessionsApi { /// * [String] id (required): /// /// * [SessionUpdateDto] sessionUpdateDto (required): - Future updateSession(String id, SessionUpdateDto sessionUpdateDto,) async { - final response = await updateSessionWithHttpInfo(id, sessionUpdateDto,); + Future updateSession(String id, SessionUpdateDto sessionUpdateDto, { Future? abortTrigger, }) async { + final response = await updateSessionWithHttpInfo(id, sessionUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 4750442287..5bd548d7d2 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -27,7 +27,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + Future addSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -50,6 +50,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -62,8 +63,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); + Future?> addSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { + final response = await addSharedLinkAssetsWithHttpInfo(id, assetIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class SharedLinksApi { /// Parameters: /// /// * [SharedLinkCreateDto] sharedLinkCreateDto (required): - Future createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto,) async { + Future createSharedLinkWithHttpInfo(SharedLinkCreateDto sharedLinkCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links'; @@ -111,6 +112,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -121,8 +123,8 @@ class SharedLinksApi { /// Parameters: /// /// * [SharedLinkCreateDto] sharedLinkCreateDto (required): - Future createSharedLink(SharedLinkCreateDto sharedLinkCreateDto,) async { - final response = await createSharedLinkWithHttpInfo(sharedLinkCreateDto,); + Future createSharedLink(SharedLinkCreateDto sharedLinkCreateDto, { Future? abortTrigger, }) async { + final response = await createSharedLinkWithHttpInfo(sharedLinkCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -149,7 +151,7 @@ class SharedLinksApi { /// /// * [String] id: /// Filter by shared link ID - Future getAllSharedLinksWithHttpInfo({ String? albumId, String? id, }) async { + Future getAllSharedLinksWithHttpInfo({ String? albumId, String? id, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links'; @@ -178,6 +180,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -192,8 +195,8 @@ class SharedLinksApi { /// /// * [String] id: /// Filter by shared link ID - Future?> getAllSharedLinks({ String? albumId, String? id, }) async { - final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, id: id, ); + Future?> getAllSharedLinks({ String? albumId, String? id, Future? abortTrigger, }) async { + final response = await getAllSharedLinksWithHttpInfo(albumId: albumId, id: id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -221,7 +224,7 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future getMySharedLinkWithHttpInfo({ String? key, String? slug, }) async { + Future getMySharedLinkWithHttpInfo({ String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/me'; @@ -250,6 +253,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -262,8 +266,8 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future getMySharedLink({ String? key, String? slug, }) async { - final response = await getMySharedLinkWithHttpInfo( key: key, slug: slug, ); + Future getMySharedLink({ String? key, String? slug, Future? abortTrigger, }) async { + final response = await getMySharedLinkWithHttpInfo(key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -286,7 +290,7 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future getSharedLinkByIdWithHttpInfo(String id,) async { + Future getSharedLinkByIdWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}' .replaceAll('{id}', id); @@ -309,6 +313,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -319,8 +324,8 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future getSharedLinkById(String id,) async { - final response = await getSharedLinkByIdWithHttpInfo(id,); + Future getSharedLinkById(String id, { Future? abortTrigger, }) async { + final response = await getSharedLinkByIdWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -343,7 +348,7 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future removeSharedLinkWithHttpInfo(String id,) async { + Future removeSharedLinkWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}' .replaceAll('{id}', id); @@ -366,6 +371,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -376,8 +382,8 @@ class SharedLinksApi { /// Parameters: /// /// * [String] id (required): - Future removeSharedLink(String id,) async { - final response = await removeSharedLinkWithHttpInfo(id,); + Future removeSharedLink(String id, { Future? abortTrigger, }) async { + final response = await removeSharedLinkWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -394,7 +400,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + Future removeSharedLinkAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}/assets' .replaceAll('{id}', id); @@ -417,6 +423,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -429,8 +436,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [AssetIdsDto] assetIdsDto (required): - Future?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto,); + Future?> removeSharedLinkAssets(String id, AssetIdsDto assetIdsDto, { Future? abortTrigger, }) async { + final response = await removeSharedLinkAssetsWithHttpInfo(id, assetIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -460,7 +467,7 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future sharedLinkLoginWithHttpInfo(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { + Future sharedLinkLoginWithHttpInfo(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/login'; @@ -489,6 +496,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -503,8 +511,8 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] slug: - Future sharedLinkLogin(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { - final response = await sharedLinkLoginWithHttpInfo(sharedLinkLoginDto, key: key, slug: slug, ); + Future sharedLinkLogin(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, Future? abortTrigger, }) async { + final response = await sharedLinkLoginWithHttpInfo(sharedLinkLoginDto, key: key, slug: slug, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -529,7 +537,7 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [SharedLinkEditDto] sharedLinkEditDto (required): - Future updateSharedLinkWithHttpInfo(String id, SharedLinkEditDto sharedLinkEditDto,) async { + Future updateSharedLinkWithHttpInfo(String id, SharedLinkEditDto sharedLinkEditDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/{id}' .replaceAll('{id}', id); @@ -552,6 +560,7 @@ class SharedLinksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -564,8 +573,8 @@ class SharedLinksApi { /// * [String] id (required): /// /// * [SharedLinkEditDto] sharedLinkEditDto (required): - Future updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto,) async { - final response = await updateSharedLinkWithHttpInfo(id, sharedLinkEditDto,); + Future updateSharedLink(String id, SharedLinkEditDto sharedLinkEditDto, { Future? abortTrigger, }) async { + final response = await updateSharedLinkWithHttpInfo(id, sharedLinkEditDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart index a691af2a7d..a99ebe0600 100644 --- a/mobile/openapi/lib/api/stacks_api.dart +++ b/mobile/openapi/lib/api/stacks_api.dart @@ -25,7 +25,7 @@ class StacksApi { /// Parameters: /// /// * [StackCreateDto] stackCreateDto (required): - Future createStackWithHttpInfo(StackCreateDto stackCreateDto,) async { + Future createStackWithHttpInfo(StackCreateDto stackCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks'; @@ -47,6 +47,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class StacksApi { /// Parameters: /// /// * [StackCreateDto] stackCreateDto (required): - Future createStack(StackCreateDto stackCreateDto,) async { - final response = await createStackWithHttpInfo(stackCreateDto,); + Future createStack(StackCreateDto stackCreateDto, { Future? abortTrigger, }) async { + final response = await createStackWithHttpInfo(stackCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future deleteStackWithHttpInfo(String id,) async { + Future deleteStackWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future deleteStack(String id,) async { - final response = await deleteStackWithHttpInfo(id,); + Future deleteStack(String id, { Future? abortTrigger, }) async { + final response = await deleteStackWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -130,7 +132,7 @@ class StacksApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteStacksWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future deleteStacksWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks'; @@ -152,6 +154,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -162,8 +165,8 @@ class StacksApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future deleteStacks(BulkIdsDto bulkIdsDto,) async { - final response = await deleteStacksWithHttpInfo(bulkIdsDto,); + Future deleteStacks(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await deleteStacksWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -178,7 +181,7 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future getStackWithHttpInfo(String id,) async { + Future getStackWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}' .replaceAll('{id}', id); @@ -201,6 +204,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -211,8 +215,8 @@ class StacksApi { /// Parameters: /// /// * [String] id (required): - Future getStack(String id,) async { - final response = await getStackWithHttpInfo(id,); + Future getStack(String id, { Future? abortTrigger, }) async { + final response = await getStackWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -237,7 +241,7 @@ class StacksApi { /// * [String] assetId (required): /// /// * [String] id (required): - Future removeAssetFromStackWithHttpInfo(String assetId, String id,) async { + Future removeAssetFromStackWithHttpInfo(String assetId, String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}/assets/{assetId}' .replaceAll('{assetId}', assetId) @@ -261,6 +265,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -273,8 +278,8 @@ class StacksApi { /// * [String] assetId (required): /// /// * [String] id (required): - Future removeAssetFromStack(String assetId, String id,) async { - final response = await removeAssetFromStackWithHttpInfo(assetId, id,); + Future removeAssetFromStack(String assetId, String id, { Future? abortTrigger, }) async { + final response = await removeAssetFromStackWithHttpInfo(assetId, id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -290,7 +295,7 @@ class StacksApi { /// /// * [String] primaryAssetId: /// Filter by primary asset ID - Future searchStacksWithHttpInfo({ String? primaryAssetId, }) async { + Future searchStacksWithHttpInfo({ String? primaryAssetId, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks'; @@ -316,6 +321,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -327,8 +333,8 @@ class StacksApi { /// /// * [String] primaryAssetId: /// Filter by primary asset ID - Future?> searchStacks({ String? primaryAssetId, }) async { - final response = await searchStacksWithHttpInfo( primaryAssetId: primaryAssetId, ); + Future?> searchStacks({ String? primaryAssetId, Future? abortTrigger, }) async { + final response = await searchStacksWithHttpInfo(primaryAssetId: primaryAssetId, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -356,7 +362,7 @@ class StacksApi { /// * [String] id (required): /// /// * [StackUpdateDto] stackUpdateDto (required): - Future updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto,) async { + Future updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks/{id}' .replaceAll('{id}', id); @@ -379,6 +385,7 @@ class StacksApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -391,8 +398,8 @@ class StacksApi { /// * [String] id (required): /// /// * [StackUpdateDto] stackUpdateDto (required): - Future updateStack(String id, StackUpdateDto stackUpdateDto,) async { - final response = await updateStackWithHttpInfo(id, stackUpdateDto,); + Future updateStack(String id, StackUpdateDto stackUpdateDto, { Future? abortTrigger, }) async { + final response = await updateStackWithHttpInfo(id, stackUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/sync_api.dart b/mobile/openapi/lib/api/sync_api.dart index e7bc822ace..c2a57c3395 100644 --- a/mobile/openapi/lib/api/sync_api.dart +++ b/mobile/openapi/lib/api/sync_api.dart @@ -25,7 +25,7 @@ class SyncApi { /// Parameters: /// /// * [SyncAckDeleteDto] syncAckDeleteDto (required): - Future deleteSyncAckWithHttpInfo(SyncAckDeleteDto syncAckDeleteDto,) async { + Future deleteSyncAckWithHttpInfo(SyncAckDeleteDto syncAckDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/ack'; @@ -47,6 +47,7 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class SyncApi { /// Parameters: /// /// * [SyncAckDeleteDto] syncAckDeleteDto (required): - Future deleteSyncAck(SyncAckDeleteDto syncAckDeleteDto,) async { - final response = await deleteSyncAckWithHttpInfo(syncAckDeleteDto,); + Future deleteSyncAck(SyncAckDeleteDto syncAckDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteSyncAckWithHttpInfo(syncAckDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class SyncApi { /// Retrieve the synchronization acknowledgments for the current session. /// /// Note: This method returns the HTTP [Response]. - Future getSyncAckWithHttpInfo() async { + Future getSyncAckWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/ack'; @@ -91,14 +92,15 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve acknowledgements /// /// Retrieve the synchronization acknowledgments for the current session. - Future?> getSyncAck() async { - final response = await getSyncAckWithHttpInfo(); + Future?> getSyncAck({ Future? abortTrigger, }) async { + final response = await getSyncAckWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -124,7 +126,7 @@ class SyncApi { /// Parameters: /// /// * [SyncStreamDto] syncStreamDto (required): - Future getSyncStreamWithHttpInfo(SyncStreamDto syncStreamDto,) async { + Future getSyncStreamWithHttpInfo(SyncStreamDto syncStreamDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/stream'; @@ -146,6 +148,7 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -156,8 +159,8 @@ class SyncApi { /// Parameters: /// /// * [SyncStreamDto] syncStreamDto (required): - Future getSyncStream(SyncStreamDto syncStreamDto,) async { - final response = await getSyncStreamWithHttpInfo(syncStreamDto,); + Future getSyncStream(SyncStreamDto syncStreamDto, { Future? abortTrigger, }) async { + final response = await getSyncStreamWithHttpInfo(syncStreamDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -172,7 +175,7 @@ class SyncApi { /// Parameters: /// /// * [SyncAckSetDto] syncAckSetDto (required): - Future sendSyncAckWithHttpInfo(SyncAckSetDto syncAckSetDto,) async { + Future sendSyncAckWithHttpInfo(SyncAckSetDto syncAckSetDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/sync/ack'; @@ -194,6 +197,7 @@ class SyncApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -204,8 +208,8 @@ class SyncApi { /// Parameters: /// /// * [SyncAckSetDto] syncAckSetDto (required): - Future sendSyncAck(SyncAckSetDto syncAckSetDto,) async { - final response = await sendSyncAckWithHttpInfo(syncAckSetDto,); + Future sendSyncAck(SyncAckSetDto syncAckSetDto, { Future? abortTrigger, }) async { + final response = await sendSyncAckWithHttpInfo(syncAckSetDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/system_config_api.dart b/mobile/openapi/lib/api/system_config_api.dart index b04da71273..ba5b82263a 100644 --- a/mobile/openapi/lib/api/system_config_api.dart +++ b/mobile/openapi/lib/api/system_config_api.dart @@ -21,7 +21,7 @@ class SystemConfigApi { /// Retrieve the current system configuration. /// /// Note: This method returns the HTTP [Response]. - Future getConfigWithHttpInfo() async { + Future getConfigWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config'; @@ -43,14 +43,15 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get system configuration /// /// Retrieve the current system configuration. - Future getConfig() async { - final response = await getConfigWithHttpInfo(); + Future getConfig({ Future? abortTrigger, }) async { + final response = await getConfigWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class SystemConfigApi { /// Retrieve the default values for the system configuration. /// /// Note: This method returns the HTTP [Response]. - Future getConfigDefaultsWithHttpInfo() async { + Future getConfigDefaultsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config/defaults'; @@ -91,14 +92,15 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get system configuration defaults /// /// Retrieve the default values for the system configuration. - Future getConfigDefaults() async { - final response = await getConfigDefaultsWithHttpInfo(); + Future getConfigDefaults({ Future? abortTrigger, }) async { + final response = await getConfigDefaultsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -117,7 +119,7 @@ class SystemConfigApi { /// Retrieve exemplary storage template options. /// /// Note: This method returns the HTTP [Response]. - Future getStorageTemplateOptionsWithHttpInfo() async { + Future getStorageTemplateOptionsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config/storage-template-options'; @@ -139,14 +141,15 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get storage template options /// /// Retrieve exemplary storage template options. - Future getStorageTemplateOptions() async { - final response = await getStorageTemplateOptionsWithHttpInfo(); + Future getStorageTemplateOptions({ Future? abortTrigger, }) async { + final response = await getStorageTemplateOptionsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -169,7 +172,7 @@ class SystemConfigApi { /// Parameters: /// /// * [SystemConfigDto] systemConfigDto (required): - Future updateConfigWithHttpInfo(SystemConfigDto systemConfigDto,) async { + Future updateConfigWithHttpInfo(SystemConfigDto systemConfigDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-config'; @@ -191,6 +194,7 @@ class SystemConfigApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -201,8 +205,8 @@ class SystemConfigApi { /// Parameters: /// /// * [SystemConfigDto] systemConfigDto (required): - Future updateConfig(SystemConfigDto systemConfigDto,) async { - final response = await updateConfigWithHttpInfo(systemConfigDto,); + Future updateConfig(SystemConfigDto systemConfigDto, { Future? abortTrigger, }) async { + final response = await updateConfigWithHttpInfo(systemConfigDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart index 63fd7628ec..a1429b54b0 100644 --- a/mobile/openapi/lib/api/system_metadata_api.dart +++ b/mobile/openapi/lib/api/system_metadata_api.dart @@ -21,7 +21,7 @@ class SystemMetadataApi { /// Retrieve the current admin onboarding status. /// /// Note: This method returns the HTTP [Response]. - Future getAdminOnboardingWithHttpInfo() async { + Future getAdminOnboardingWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/admin-onboarding'; @@ -43,14 +43,15 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve admin onboarding /// /// Retrieve the current admin onboarding status. - Future getAdminOnboarding() async { - final response = await getAdminOnboardingWithHttpInfo(); + Future getAdminOnboarding({ Future? abortTrigger, }) async { + final response = await getAdminOnboardingWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -69,7 +70,7 @@ class SystemMetadataApi { /// Retrieve the current state of the reverse geocoding import. /// /// Note: This method returns the HTTP [Response]. - Future getReverseGeocodingStateWithHttpInfo() async { + Future getReverseGeocodingStateWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/reverse-geocoding-state'; @@ -91,14 +92,15 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve reverse geocoding state /// /// Retrieve the current state of the reverse geocoding import. - Future getReverseGeocodingState() async { - final response = await getReverseGeocodingStateWithHttpInfo(); + Future getReverseGeocodingState({ Future? abortTrigger, }) async { + final response = await getReverseGeocodingStateWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -117,7 +119,7 @@ class SystemMetadataApi { /// Retrieve the current state of the version check process. /// /// Note: This method returns the HTTP [Response]. - Future getVersionCheckStateWithHttpInfo() async { + Future getVersionCheckStateWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/version-check-state'; @@ -139,14 +141,15 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve version check state /// /// Retrieve the current state of the version check process. - Future getVersionCheckState() async { - final response = await getVersionCheckStateWithHttpInfo(); + Future getVersionCheckState({ Future? abortTrigger, }) async { + final response = await getVersionCheckStateWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -169,7 +172,7 @@ class SystemMetadataApi { /// Parameters: /// /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): - Future updateAdminOnboardingWithHttpInfo(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + Future updateAdminOnboardingWithHttpInfo(AdminOnboardingUpdateDto adminOnboardingUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/system-metadata/admin-onboarding'; @@ -191,6 +194,7 @@ class SystemMetadataApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -201,8 +205,8 @@ class SystemMetadataApi { /// Parameters: /// /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): - Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { - final response = await updateAdminOnboardingWithHttpInfo(adminOnboardingUpdateDto,); + Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto, { Future? abortTrigger, }) async { + final response = await updateAdminOnboardingWithHttpInfo(adminOnboardingUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/tags_api.dart b/mobile/openapi/lib/api/tags_api.dart index a6840f9483..c3cf9f545c 100644 --- a/mobile/openapi/lib/api/tags_api.dart +++ b/mobile/openapi/lib/api/tags_api.dart @@ -25,7 +25,7 @@ class TagsApi { /// Parameters: /// /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): - Future bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto,) async { + Future bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/assets'; @@ -47,6 +47,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class TagsApi { /// Parameters: /// /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): - Future bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto,) async { - final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto,); + Future bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto, { Future? abortTrigger, }) async { + final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class TagsApi { /// Parameters: /// /// * [TagCreateDto] tagCreateDto (required): - Future createTagWithHttpInfo(TagCreateDto tagCreateDto,) async { + Future createTagWithHttpInfo(TagCreateDto tagCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags'; @@ -103,6 +104,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -113,8 +115,8 @@ class TagsApi { /// Parameters: /// /// * [TagCreateDto] tagCreateDto (required): - Future createTag(TagCreateDto tagCreateDto,) async { - final response = await createTagWithHttpInfo(tagCreateDto,); + Future createTag(TagCreateDto tagCreateDto, { Future? abortTrigger, }) async { + final response = await createTagWithHttpInfo(tagCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -137,7 +139,7 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future deleteTagWithHttpInfo(String id,) async { + Future deleteTagWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}' .replaceAll('{id}', id); @@ -160,6 +162,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -170,8 +173,8 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future deleteTag(String id,) async { - final response = await deleteTagWithHttpInfo(id,); + Future deleteTag(String id, { Future? abortTrigger, }) async { + final response = await deleteTagWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -182,7 +185,7 @@ class TagsApi { /// Retrieve a list of all tags. /// /// Note: This method returns the HTTP [Response]. - Future getAllTagsWithHttpInfo() async { + Future getAllTagsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags'; @@ -204,14 +207,15 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve tags /// /// Retrieve a list of all tags. - Future?> getAllTags() async { - final response = await getAllTagsWithHttpInfo(); + Future?> getAllTags({ Future? abortTrigger, }) async { + final response = await getAllTagsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -237,7 +241,7 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future getTagByIdWithHttpInfo(String id,) async { + Future getTagByIdWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}' .replaceAll('{id}', id); @@ -260,6 +264,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -270,8 +275,8 @@ class TagsApi { /// Parameters: /// /// * [String] id (required): - Future getTagById(String id,) async { - final response = await getTagByIdWithHttpInfo(id,); + Future getTagById(String id, { Future? abortTrigger, }) async { + final response = await getTagByIdWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -296,7 +301,7 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}/assets' .replaceAll('{id}', id); @@ -319,6 +324,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -331,8 +337,8 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> tagAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await tagAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> tagAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await tagAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -360,7 +366,7 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + Future untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}/assets' .replaceAll('{id}', id); @@ -383,6 +389,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -395,8 +402,8 @@ class TagsApi { /// * [String] id (required): /// /// * [BulkIdsDto] bulkIdsDto (required): - Future?> untagAssets(String id, BulkIdsDto bulkIdsDto,) async { - final response = await untagAssetsWithHttpInfo(id, bulkIdsDto,); + Future?> untagAssets(String id, BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await untagAssetsWithHttpInfo(id, bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -424,7 +431,7 @@ class TagsApi { /// * [String] id (required): /// /// * [TagUpdateDto] tagUpdateDto (required): - Future updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto,) async { + Future updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags/{id}' .replaceAll('{id}', id); @@ -447,6 +454,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -459,8 +467,8 @@ class TagsApi { /// * [String] id (required): /// /// * [TagUpdateDto] tagUpdateDto (required): - Future updateTag(String id, TagUpdateDto tagUpdateDto,) async { - final response = await updateTagWithHttpInfo(id, tagUpdateDto,); + Future updateTag(String id, TagUpdateDto tagUpdateDto, { Future? abortTrigger, }) async { + final response = await updateTagWithHttpInfo(id, tagUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -483,7 +491,7 @@ class TagsApi { /// Parameters: /// /// * [TagUpsertDto] tagUpsertDto (required): - Future upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto,) async { + Future upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/tags'; @@ -505,6 +513,7 @@ class TagsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -515,8 +524,8 @@ class TagsApi { /// Parameters: /// /// * [TagUpsertDto] tagUpsertDto (required): - Future?> upsertTags(TagUpsertDto tagUpsertDto,) async { - final response = await upsertTagsWithHttpInfo(tagUpsertDto,); + Future?> upsertTags(TagUpsertDto tagUpsertDto, { Future? abortTrigger, }) async { + final response = await upsertTagsWithHttpInfo(tagUpsertDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index 6c72f62604..a85aee2d7a 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -69,7 +69,7 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/bucket'; @@ -138,6 +138,7 @@ class TimelineApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -192,8 +193,8 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); + Future getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { + final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -257,7 +258,7 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/buckets'; @@ -325,6 +326,7 @@ class TimelineApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -376,8 +378,8 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo( albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, Future? abortTrigger, }) async { + final response = await getTimeBucketsWithHttpInfo(albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/trash_api.dart b/mobile/openapi/lib/api/trash_api.dart index f1dcbb8896..7b593e5111 100644 --- a/mobile/openapi/lib/api/trash_api.dart +++ b/mobile/openapi/lib/api/trash_api.dart @@ -21,7 +21,7 @@ class TrashApi { /// Permanently delete all items in the trash. /// /// Note: This method returns the HTTP [Response]. - Future emptyTrashWithHttpInfo() async { + Future emptyTrashWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/trash/empty'; @@ -43,14 +43,15 @@ class TrashApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Empty trash /// /// Permanently delete all items in the trash. - Future emptyTrash() async { - final response = await emptyTrashWithHttpInfo(); + Future emptyTrash({ Future? abortTrigger, }) async { + final response = await emptyTrashWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -73,7 +74,7 @@ class TrashApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + Future restoreAssetsWithHttpInfo(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/trash/restore/assets'; @@ -95,6 +96,7 @@ class TrashApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -105,8 +107,8 @@ class TrashApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssets(BulkIdsDto bulkIdsDto,) async { - final response = await restoreAssetsWithHttpInfo(bulkIdsDto,); + Future restoreAssets(BulkIdsDto bulkIdsDto, { Future? abortTrigger, }) async { + final response = await restoreAssetsWithHttpInfo(bulkIdsDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -125,7 +127,7 @@ class TrashApi { /// Restore all items in the trash. /// /// Note: This method returns the HTTP [Response]. - Future restoreTrashWithHttpInfo() async { + Future restoreTrashWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/trash/restore'; @@ -147,14 +149,15 @@ class TrashApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Restore trash /// /// Restore all items in the trash. - Future restoreTrash() async { - final response = await restoreTrashWithHttpInfo(); + Future restoreTrash({ Future? abortTrigger, }) async { + final response = await restoreTrashWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index 5e165ffd5d..fd6b43d9ce 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -25,7 +25,7 @@ class UsersAdminApi { /// Parameters: /// /// * [UserAdminCreateDto] userAdminCreateDto (required): - Future createUserAdminWithHttpInfo(UserAdminCreateDto userAdminCreateDto,) async { + Future createUserAdminWithHttpInfo(UserAdminCreateDto userAdminCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users'; @@ -47,6 +47,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class UsersAdminApi { /// Parameters: /// /// * [UserAdminCreateDto] userAdminCreateDto (required): - Future createUserAdmin(UserAdminCreateDto userAdminCreateDto,) async { - final response = await createUserAdminWithHttpInfo(userAdminCreateDto,); + Future createUserAdmin(UserAdminCreateDto userAdminCreateDto, { Future? abortTrigger, }) async { + final response = await createUserAdminWithHttpInfo(userAdminCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -83,7 +84,7 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminDeleteDto] userAdminDeleteDto (required): - Future deleteUserAdminWithHttpInfo(String id, UserAdminDeleteDto userAdminDeleteDto,) async { + Future deleteUserAdminWithHttpInfo(String id, UserAdminDeleteDto userAdminDeleteDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}' .replaceAll('{id}', id); @@ -106,6 +107,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -118,8 +120,8 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminDeleteDto] userAdminDeleteDto (required): - Future deleteUserAdmin(String id, UserAdminDeleteDto userAdminDeleteDto,) async { - final response = await deleteUserAdminWithHttpInfo(id, userAdminDeleteDto,); + Future deleteUserAdmin(String id, UserAdminDeleteDto userAdminDeleteDto, { Future? abortTrigger, }) async { + final response = await deleteUserAdminWithHttpInfo(id, userAdminDeleteDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -142,7 +144,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserAdminWithHttpInfo(String id,) async { + Future getUserAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}' .replaceAll('{id}', id); @@ -165,6 +167,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -175,8 +178,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserAdmin(String id,) async { - final response = await getUserAdminWithHttpInfo(id,); + Future getUserAdmin(String id, { Future? abortTrigger, }) async { + final response = await getUserAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -199,7 +202,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserPreferencesAdminWithHttpInfo(String id,) async { + Future getUserPreferencesAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/preferences' .replaceAll('{id}', id); @@ -222,6 +225,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -232,8 +236,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserPreferencesAdmin(String id,) async { - final response = await getUserPreferencesAdminWithHttpInfo(id,); + Future getUserPreferencesAdmin(String id, { Future? abortTrigger, }) async { + final response = await getUserPreferencesAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -256,7 +260,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future getUserSessionsAdminWithHttpInfo(String id,) async { + Future getUserSessionsAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/sessions' .replaceAll('{id}', id); @@ -279,6 +283,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -289,8 +294,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future?> getUserSessionsAdmin(String id,) async { - final response = await getUserSessionsAdminWithHttpInfo(id,); + Future?> getUserSessionsAdmin(String id, { Future? abortTrigger, }) async { + final response = await getUserSessionsAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -324,7 +329,7 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { + Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/statistics' .replaceAll('{id}', id); @@ -357,6 +362,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -375,8 +381,8 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { - final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); + Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, Future? abortTrigger, }) async { + final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -399,7 +405,7 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future restoreUserAdminWithHttpInfo(String id,) async { + Future restoreUserAdminWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/restore' .replaceAll('{id}', id); @@ -422,6 +428,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -432,8 +439,8 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id (required): - Future restoreUserAdmin(String id,) async { - final response = await restoreUserAdminWithHttpInfo(id,); + Future restoreUserAdmin(String id, { Future? abortTrigger, }) async { + final response = await restoreUserAdminWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -460,7 +467,7 @@ class UsersAdminApi { /// /// * [bool] withDeleted: /// Include deleted users - Future searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, }) async { + Future searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users'; @@ -489,6 +496,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -503,8 +511,8 @@ class UsersAdminApi { /// /// * [bool] withDeleted: /// Include deleted users - Future?> searchUsersAdmin({ String? id, bool? withDeleted, }) async { - final response = await searchUsersAdminWithHttpInfo( id: id, withDeleted: withDeleted, ); + Future?> searchUsersAdmin({ String? id, bool? withDeleted, Future? abortTrigger, }) async { + final response = await searchUsersAdminWithHttpInfo(id: id, withDeleted: withDeleted, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -532,7 +540,7 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminUpdateDto] userAdminUpdateDto (required): - Future updateUserAdminWithHttpInfo(String id, UserAdminUpdateDto userAdminUpdateDto,) async { + Future updateUserAdminWithHttpInfo(String id, UserAdminUpdateDto userAdminUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}' .replaceAll('{id}', id); @@ -555,6 +563,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -567,8 +576,8 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserAdminUpdateDto] userAdminUpdateDto (required): - Future updateUserAdmin(String id, UserAdminUpdateDto userAdminUpdateDto,) async { - final response = await updateUserAdminWithHttpInfo(id, userAdminUpdateDto,); + Future updateUserAdmin(String id, UserAdminUpdateDto userAdminUpdateDto, { Future? abortTrigger, }) async { + final response = await updateUserAdminWithHttpInfo(id, userAdminUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -593,7 +602,7 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateUserPreferencesAdminWithHttpInfo(String id, UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + Future updateUserPreferencesAdminWithHttpInfo(String id, UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/preferences' .replaceAll('{id}', id); @@ -616,6 +625,7 @@ class UsersAdminApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -628,8 +638,8 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateUserPreferencesAdmin(String id, UserPreferencesUpdateDto userPreferencesUpdateDto,) async { - final response = await updateUserPreferencesAdminWithHttpInfo(id, userPreferencesUpdateDto,); + Future updateUserPreferencesAdmin(String id, UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { + final response = await updateUserPreferencesAdminWithHttpInfo(id, userPreferencesUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index 401cf4e94b..a7fac3ea66 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -26,7 +26,7 @@ class UsersApi { /// /// * [MultipartFile] file (required): /// Profile image file - Future createProfileImageWithHttpInfo(MultipartFile file,) async { + Future createProfileImageWithHttpInfo(MultipartFile file, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/profile-image'; @@ -58,6 +58,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -69,8 +70,8 @@ class UsersApi { /// /// * [MultipartFile] file (required): /// Profile image file - Future createProfileImage(MultipartFile file,) async { - final response = await createProfileImageWithHttpInfo(file,); + Future createProfileImage(MultipartFile file, { Future? abortTrigger, }) async { + final response = await createProfileImageWithHttpInfo(file, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -89,7 +90,7 @@ class UsersApi { /// Delete the profile image of the current user. /// /// Note: This method returns the HTTP [Response]. - Future deleteProfileImageWithHttpInfo() async { + Future deleteProfileImageWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/profile-image'; @@ -111,14 +112,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete user profile image /// /// Delete the profile image of the current user. - Future deleteProfileImage() async { - final response = await deleteProfileImageWithHttpInfo(); + Future deleteProfileImage({ Future? abortTrigger, }) async { + final response = await deleteProfileImageWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -129,7 +131,7 @@ class UsersApi { /// Delete the registered product key for the current user. /// /// Note: This method returns the HTTP [Response]. - Future deleteUserLicenseWithHttpInfo() async { + Future deleteUserLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/license'; @@ -151,14 +153,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete user product key /// /// Delete the registered product key for the current user. - Future deleteUserLicense() async { - final response = await deleteUserLicenseWithHttpInfo(); + Future deleteUserLicense({ Future? abortTrigger, }) async { + final response = await deleteUserLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -169,7 +172,7 @@ class UsersApi { /// Delete the onboarding status of the current user. /// /// Note: This method returns the HTTP [Response]. - Future deleteUserOnboardingWithHttpInfo() async { + Future deleteUserOnboardingWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/onboarding'; @@ -191,14 +194,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Delete user onboarding /// /// Delete the onboarding status of the current user. - Future deleteUserOnboarding() async { - final response = await deleteUserOnboardingWithHttpInfo(); + Future deleteUserOnboarding({ Future? abortTrigger, }) async { + final response = await deleteUserOnboardingWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -209,7 +213,7 @@ class UsersApi { /// Retrieve the preferences for the current user. /// /// Note: This method returns the HTTP [Response]. - Future getMyPreferencesWithHttpInfo() async { + Future getMyPreferencesWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/preferences'; @@ -231,14 +235,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get my preferences /// /// Retrieve the preferences for the current user. - Future getMyPreferences() async { - final response = await getMyPreferencesWithHttpInfo(); + Future getMyPreferences({ Future? abortTrigger, }) async { + final response = await getMyPreferencesWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -257,7 +262,7 @@ class UsersApi { /// Retrieve information about the user making the API request. /// /// Note: This method returns the HTTP [Response]. - Future getMyUserWithHttpInfo() async { + Future getMyUserWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me'; @@ -279,14 +284,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get current user /// /// Retrieve information about the user making the API request. - Future getMyUser() async { - final response = await getMyUserWithHttpInfo(); + Future getMyUser({ Future? abortTrigger, }) async { + final response = await getMyUserWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -309,7 +315,7 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getProfileImageWithHttpInfo(String id,) async { + Future getProfileImageWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/{id}/profile-image' .replaceAll('{id}', id); @@ -332,6 +338,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -342,8 +349,8 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getProfileImage(String id,) async { - final response = await getProfileImageWithHttpInfo(id,); + Future getProfileImage(String id, { Future? abortTrigger, }) async { + final response = await getProfileImageWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -366,7 +373,7 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getUserWithHttpInfo(String id,) async { + Future getUserWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/{id}' .replaceAll('{id}', id); @@ -389,6 +396,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -399,8 +407,8 @@ class UsersApi { /// Parameters: /// /// * [String] id (required): - Future getUser(String id,) async { - final response = await getUserWithHttpInfo(id,); + Future getUser(String id, { Future? abortTrigger, }) async { + final response = await getUserWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -419,7 +427,7 @@ class UsersApi { /// Retrieve information about whether the current user has a registered product key. /// /// Note: This method returns the HTTP [Response]. - Future getUserLicenseWithHttpInfo() async { + Future getUserLicenseWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/license'; @@ -441,14 +449,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve user product key /// /// Retrieve information about whether the current user has a registered product key. - Future getUserLicense() async { - final response = await getUserLicenseWithHttpInfo(); + Future getUserLicense({ Future? abortTrigger, }) async { + final response = await getUserLicenseWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -467,7 +476,7 @@ class UsersApi { /// Retrieve the onboarding status of the current user. /// /// Note: This method returns the HTTP [Response]. - Future getUserOnboardingWithHttpInfo() async { + Future getUserOnboardingWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/onboarding'; @@ -489,14 +498,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve user onboarding /// /// Retrieve the onboarding status of the current user. - Future getUserOnboarding() async { - final response = await getUserOnboardingWithHttpInfo(); + Future getUserOnboarding({ Future? abortTrigger, }) async { + final response = await getUserOnboardingWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -515,7 +525,7 @@ class UsersApi { /// Retrieve a list of all users on the server. /// /// Note: This method returns the HTTP [Response]. - Future searchUsersWithHttpInfo() async { + Future searchUsersWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users'; @@ -537,14 +547,15 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Get all users /// /// Retrieve a list of all users on the server. - Future?> searchUsers() async { - final response = await searchUsersWithHttpInfo(); + Future?> searchUsers({ Future? abortTrigger, }) async { + final response = await searchUsersWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -570,7 +581,7 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto,) async { + Future setUserLicenseWithHttpInfo(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/license'; @@ -592,6 +603,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -602,8 +614,8 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { - final response = await setUserLicenseWithHttpInfo(licenseKeyDto,); + Future setUserLicense(LicenseKeyDto licenseKeyDto, { Future? abortTrigger, }) async { + final response = await setUserLicenseWithHttpInfo(licenseKeyDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -626,7 +638,7 @@ class UsersApi { /// Parameters: /// /// * [OnboardingDto] onboardingDto (required): - Future setUserOnboardingWithHttpInfo(OnboardingDto onboardingDto,) async { + Future setUserOnboardingWithHttpInfo(OnboardingDto onboardingDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/onboarding'; @@ -648,6 +660,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -658,8 +671,8 @@ class UsersApi { /// Parameters: /// /// * [OnboardingDto] onboardingDto (required): - Future setUserOnboarding(OnboardingDto onboardingDto,) async { - final response = await setUserOnboardingWithHttpInfo(onboardingDto,); + Future setUserOnboarding(OnboardingDto onboardingDto, { Future? abortTrigger, }) async { + final response = await setUserOnboardingWithHttpInfo(onboardingDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -682,7 +695,7 @@ class UsersApi { /// Parameters: /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateMyPreferencesWithHttpInfo(UserPreferencesUpdateDto userPreferencesUpdateDto,) async { + Future updateMyPreferencesWithHttpInfo(UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me/preferences'; @@ -704,6 +717,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -714,8 +728,8 @@ class UsersApi { /// Parameters: /// /// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required): - Future updateMyPreferences(UserPreferencesUpdateDto userPreferencesUpdateDto,) async { - final response = await updateMyPreferencesWithHttpInfo(userPreferencesUpdateDto,); + Future updateMyPreferences(UserPreferencesUpdateDto userPreferencesUpdateDto, { Future? abortTrigger, }) async { + final response = await updateMyPreferencesWithHttpInfo(userPreferencesUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -738,7 +752,7 @@ class UsersApi { /// Parameters: /// /// * [UserUpdateMeDto] userUpdateMeDto (required): - Future updateMyUserWithHttpInfo(UserUpdateMeDto userUpdateMeDto,) async { + Future updateMyUserWithHttpInfo(UserUpdateMeDto userUpdateMeDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/users/me'; @@ -760,6 +774,7 @@ class UsersApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -770,8 +785,8 @@ class UsersApi { /// Parameters: /// /// * [UserUpdateMeDto] userUpdateMeDto (required): - Future updateMyUser(UserUpdateMeDto userUpdateMeDto,) async { - final response = await updateMyUserWithHttpInfo(userUpdateMeDto,); + Future updateMyUser(UserUpdateMeDto userUpdateMeDto, { Future? abortTrigger, }) async { + final response = await updateMyUserWithHttpInfo(userUpdateMeDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/views_api.dart b/mobile/openapi/lib/api/views_api.dart index a45e89d58f..3ccbacb650 100644 --- a/mobile/openapi/lib/api/views_api.dart +++ b/mobile/openapi/lib/api/views_api.dart @@ -25,7 +25,7 @@ class ViewsApi { /// Parameters: /// /// * [String] path (required): - Future getAssetsByOriginalPathWithHttpInfo(String path,) async { + Future getAssetsByOriginalPathWithHttpInfo(String path, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/view/folder'; @@ -49,6 +49,7 @@ class ViewsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -59,8 +60,8 @@ class ViewsApi { /// Parameters: /// /// * [String] path (required): - Future?> getAssetsByOriginalPath(String path,) async { - final response = await getAssetsByOriginalPathWithHttpInfo(path,); + Future?> getAssetsByOriginalPath(String path, { Future? abortTrigger, }) async { + final response = await getAssetsByOriginalPathWithHttpInfo(path, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -82,7 +83,7 @@ class ViewsApi { /// Retrieve a list of unique folder paths from asset original paths. /// /// Note: This method returns the HTTP [Response]. - Future getUniqueOriginalPathsWithHttpInfo() async { + Future getUniqueOriginalPathsWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/view/folder/unique-paths'; @@ -104,14 +105,15 @@ class ViewsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// Retrieve unique paths /// /// Retrieve a list of unique folder paths from asset original paths. - Future?> getUniqueOriginalPaths() async { - final response = await getUniqueOriginalPathsWithHttpInfo(); + Future?> getUniqueOriginalPaths({ Future? abortTrigger, }) async { + final response = await getUniqueOriginalPathsWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/workflows_api.dart b/mobile/openapi/lib/api/workflows_api.dart index 12b33b7238..4b27acd624 100644 --- a/mobile/openapi/lib/api/workflows_api.dart +++ b/mobile/openapi/lib/api/workflows_api.dart @@ -25,7 +25,7 @@ class WorkflowsApi { /// Parameters: /// /// * [WorkflowCreateDto] workflowCreateDto (required): - Future createWorkflowWithHttpInfo(WorkflowCreateDto workflowCreateDto,) async { + Future createWorkflowWithHttpInfo(WorkflowCreateDto workflowCreateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows'; @@ -47,6 +47,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -57,8 +58,8 @@ class WorkflowsApi { /// Parameters: /// /// * [WorkflowCreateDto] workflowCreateDto (required): - Future createWorkflow(WorkflowCreateDto workflowCreateDto,) async { - final response = await createWorkflowWithHttpInfo(workflowCreateDto,); + Future createWorkflow(WorkflowCreateDto workflowCreateDto, { Future? abortTrigger, }) async { + final response = await createWorkflowWithHttpInfo(workflowCreateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -81,7 +82,7 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future deleteWorkflowWithHttpInfo(String id,) async { + Future deleteWorkflowWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}' .replaceAll('{id}', id); @@ -104,6 +105,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -114,8 +116,8 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future deleteWorkflow(String id,) async { - final response = await deleteWorkflowWithHttpInfo(id,); + Future deleteWorkflow(String id, { Future? abortTrigger, }) async { + final response = await deleteWorkflowWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -130,7 +132,7 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflowWithHttpInfo(String id,) async { + Future getWorkflowWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}' .replaceAll('{id}', id); @@ -153,6 +155,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -163,8 +166,8 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflow(String id,) async { - final response = await getWorkflowWithHttpInfo(id,); + Future getWorkflow(String id, { Future? abortTrigger, }) async { + final response = await getWorkflowWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -187,7 +190,7 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflowForShareWithHttpInfo(String id,) async { + Future getWorkflowForShareWithHttpInfo(String id, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}/share' .replaceAll('{id}', id); @@ -210,6 +213,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -220,8 +224,8 @@ class WorkflowsApi { /// Parameters: /// /// * [String] id (required): - Future getWorkflowForShare(String id,) async { - final response = await getWorkflowForShareWithHttpInfo(id,); + Future getWorkflowForShare(String id, { Future? abortTrigger, }) async { + final response = await getWorkflowForShareWithHttpInfo(id, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -240,7 +244,7 @@ class WorkflowsApi { /// Retrieve a list of all available workflow triggers. /// /// Note: This method returns the HTTP [Response]. - Future getWorkflowTriggersWithHttpInfo() async { + Future getWorkflowTriggersWithHttpInfo({ Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/triggers'; @@ -262,14 +266,15 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } /// List all workflow triggers /// /// Retrieve a list of all available workflow triggers. - Future?> getWorkflowTriggers() async { - final response = await getWorkflowTriggersWithHttpInfo(); + Future?> getWorkflowTriggers({ Future? abortTrigger, }) async { + final response = await getWorkflowTriggersWithHttpInfo(abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -308,7 +313,7 @@ class WorkflowsApi { /// /// * [WorkflowTrigger] trigger: /// Workflow trigger type - Future searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async { + Future searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows'; @@ -346,6 +351,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -369,8 +375,8 @@ class WorkflowsApi { /// /// * [WorkflowTrigger] trigger: /// Workflow trigger type - Future?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async { - final response = await searchWorkflowsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, trigger: trigger, ); + Future?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future? abortTrigger, }) async { + final response = await searchWorkflowsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, trigger: trigger, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -398,7 +404,7 @@ class WorkflowsApi { /// * [String] id (required): /// /// * [WorkflowUpdateDto] workflowUpdateDto (required): - Future updateWorkflowWithHttpInfo(String id, WorkflowUpdateDto workflowUpdateDto,) async { + Future updateWorkflowWithHttpInfo(String id, WorkflowUpdateDto workflowUpdateDto, { Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/workflows/{id}' .replaceAll('{id}', id); @@ -421,6 +427,7 @@ class WorkflowsApi { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -433,8 +440,8 @@ class WorkflowsApi { /// * [String] id (required): /// /// * [WorkflowUpdateDto] workflowUpdateDto (required): - Future updateWorkflow(String id, WorkflowUpdateDto workflowUpdateDto,) async { - final response = await updateWorkflowWithHttpInfo(id, workflowUpdateDto,); + Future updateWorkflow(String id, WorkflowUpdateDto workflowUpdateDto, { Future? abortTrigger, }) async { + final response = await updateWorkflowWithHttpInfo(id, workflowUpdateDto, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a3c2369c1d..6efa46571e 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -13,7 +13,7 @@ part of openapi.api; class ApiClient { ApiClient({this.basePath = '/api', this.authentication,}); - final String basePath; + String basePath; final Authentication? authentication; var _client = Client(); @@ -44,8 +44,9 @@ class ApiClient { Object? body, Map headerParams, Map formParams, - String? contentType, - ) async { + String? contentType, { + Future? abortTrigger, + }) async { await authentication?.applyToParams(queryParams, headerParams); headerParams.addAll(_defaultHeaderMap); @@ -63,7 +64,7 @@ class ApiClient { body is MultipartFile && (contentType == null || !contentType.toLowerCase().startsWith('multipart/form-data')) ) { - final request = StreamedRequest(method, uri); + final request = AbortableStreamedRequest(method, uri, abortTrigger: abortTrigger); request.headers.addAll(headerParams); request.contentLength = body.length; body.finalize().listen( @@ -78,7 +79,7 @@ class ApiClient { } if (body is MultipartRequest) { - final request = MultipartRequest(method, uri); + final request = AbortableMultipartRequest(method, uri, abortTrigger: abortTrigger); request.fields.addAll(body.fields); request.files.addAll(body.files); request.headers.addAll(body.headers); @@ -92,14 +93,19 @@ class ApiClient { : await serializeAsync(body); final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; - switch(method) { - case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); - case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); - case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); - case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); + final request = AbortableRequest(method, uri, abortTrigger: abortTrigger); + if (nullableHeaderParams != null) { + request.headers.addAll(nullableHeaderParams); } + if (msgBody is String) { + request.body = msgBody; + } else if (msgBody is List) { + request.bodyBytes = msgBody; + } else if (msgBody is Map) { + request.bodyFields = msgBody; + } + final response = await _client.send(request); + return Response.fromStream(response); } on SocketException catch (error, trace) { throw ApiException.withInner( HttpStatus.badRequest, @@ -136,11 +142,6 @@ class ApiClient { trace, ); } - - throw ApiException( - HttpStatus.badRequest, - 'Invalid HTTP operation: $method $path', - ); } Future deserializeAsync(String value, String targetType, {bool growable = false,}) => @@ -562,6 +563,12 @@ class ApiClient { return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': return ReactionTypeTypeTransformer().decode(value); + case 'ReleaseChannel': + return ReleaseChannelTypeTransformer().decode(value); + case 'ReleaseEventV1': + return ReleaseEventV1.fromJson(value); + case 'ReleaseType': + return ReleaseTypeTypeTransformer().decode(value); case 'ReverseGeocodingStateResponseDto': return ReverseGeocodingStateResponseDto.fromJson(value); case 'RotateParameters': @@ -730,6 +737,8 @@ class ApiClient { return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': return SystemConfigFFmpegDto.fromJson(value); + case 'SystemConfigFFmpegRealtimeDto': + return SystemConfigFFmpegRealtimeDto.fromJson(value); case 'SystemConfigFacesDto': return SystemConfigFacesDto.fromJson(value); case 'SystemConfigGeneratedFullsizeImageDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index b5d348edd6..6cf11022c3 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -157,6 +157,12 @@ String parameterToString(dynamic value) { if (value is ReactionType) { return ReactionTypeTypeTransformer().encode(value).toString(); } + if (value is ReleaseChannel) { + return ReleaseChannelTypeTransformer().encode(value).toString(); + } + if (value is ReleaseType) { + return ReleaseTypeTypeTransformer().encode(value).toString(); + } if (value is SearchSuggestionType) { return SearchSuggestionTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 444b080c12..435cffd623 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -52,6 +52,7 @@ class JobName { static const librarySyncFilesQueueAll = JobName._(r'LibrarySyncFilesQueueAll'); static const librarySyncFiles = JobName._(r'LibrarySyncFiles'); static const libraryScanQueueAll = JobName._(r'LibraryScanQueueAll'); + static const hlsSessionCleanup = JobName._(r'HlsSessionCleanup'); static const memoryCleanup = JobName._(r'MemoryCleanup'); static const memoryGenerate = JobName._(r'MemoryGenerate'); static const notificationsCleanup = JobName._(r'NotificationsCleanup'); @@ -77,7 +78,7 @@ class JobName { static const versionCheck = JobName._(r'VersionCheck'); static const ocrQueueAll = JobName._(r'OcrQueueAll'); static const ocr = JobName._(r'Ocr'); - static const workflowAssetCreate = JobName._(r'WorkflowAssetCreate'); + static const workflowAssetTrigger = JobName._(r'WorkflowAssetTrigger'); /// List of all possible values in this [enum][JobName]. static const values = [ @@ -110,6 +111,7 @@ class JobName { librarySyncFilesQueueAll, librarySyncFiles, libraryScanQueueAll, + hlsSessionCleanup, memoryCleanup, memoryGenerate, notificationsCleanup, @@ -135,7 +137,7 @@ class JobName { versionCheck, ocrQueueAll, ocr, - workflowAssetCreate, + workflowAssetTrigger, ]; static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); @@ -203,6 +205,7 @@ class JobNameTypeTransformer { case r'LibrarySyncFilesQueueAll': return JobName.librarySyncFilesQueueAll; case r'LibrarySyncFiles': return JobName.librarySyncFiles; case r'LibraryScanQueueAll': return JobName.libraryScanQueueAll; + case r'HlsSessionCleanup': return JobName.hlsSessionCleanup; case r'MemoryCleanup': return JobName.memoryCleanup; case r'MemoryGenerate': return JobName.memoryGenerate; case r'NotificationsCleanup': return JobName.notificationsCleanup; @@ -228,7 +231,7 @@ class JobNameTypeTransformer { case r'VersionCheck': return JobName.versionCheck; case r'OcrQueueAll': return JobName.ocrQueueAll; case r'Ocr': return JobName.ocr; - case r'WorkflowAssetCreate': return JobName.workflowAssetCreate; + case r'WorkflowAssetTrigger': return JobName.workflowAssetTrigger; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/plugin_template_response_dto.dart b/mobile/openapi/lib/model/plugin_template_response_dto.dart index 4625da37d3..9f54753f49 100644 --- a/mobile/openapi/lib/model/plugin_template_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_template_response_dto.dart @@ -18,6 +18,7 @@ class PluginTemplateResponseDto { this.steps = const [], required this.title, required this.trigger, + this.uiHints = const [], }); /// Template description @@ -34,13 +35,17 @@ class PluginTemplateResponseDto { WorkflowTrigger trigger; + /// Ui hints, for example \"smart-album\" + List uiHints; + @override bool operator ==(Object other) => identical(this, other) || other is PluginTemplateResponseDto && other.description == description && other.key == key && _deepEquality.equals(other.steps, steps) && other.title == title && - other.trigger == trigger; + other.trigger == trigger && + _deepEquality.equals(other.uiHints, uiHints); @override int get hashCode => @@ -49,10 +54,11 @@ class PluginTemplateResponseDto { (key.hashCode) + (steps.hashCode) + (title.hashCode) + - (trigger.hashCode); + (trigger.hashCode) + + (uiHints.hashCode); @override - String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger]'; + String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger, uiHints=$uiHints]'; Map toJson() { final json = {}; @@ -61,6 +67,7 @@ class PluginTemplateResponseDto { json[r'steps'] = this.steps; json[r'title'] = this.title; json[r'trigger'] = this.trigger; + json[r'uiHints'] = this.uiHints; return json; } @@ -78,6 +85,9 @@ class PluginTemplateResponseDto { steps: PluginTemplateStepResponseDto.listFromJson(json[r'steps']), title: mapValueOfType(json, r'title')!, trigger: WorkflowTrigger.fromJson(json[r'trigger'])!, + uiHints: json[r'uiHints'] is Iterable + ? (json[r'uiHints'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; @@ -130,6 +140,7 @@ class PluginTemplateResponseDto { 'steps', 'title', 'trigger', + 'uiHints', }; } diff --git a/mobile/openapi/lib/model/release_channel.dart b/mobile/openapi/lib/model/release_channel.dart new file mode 100644 index 0000000000..48b082af07 --- /dev/null +++ b/mobile/openapi/lib/model/release_channel.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Release channel +class ReleaseChannel { + /// Instantiate a new enum with the provided [value]. + const ReleaseChannel._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const stable = ReleaseChannel._(r'stable'); + static const releaseCandidate = ReleaseChannel._(r'releaseCandidate'); + + /// List of all possible values in this [enum][ReleaseChannel]. + static const values = [ + stable, + releaseCandidate, + ]; + + static ReleaseChannel? fromJson(dynamic value) => ReleaseChannelTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReleaseChannel.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReleaseChannel] to String, +/// and [decode] dynamic data back to [ReleaseChannel]. +class ReleaseChannelTypeTransformer { + factory ReleaseChannelTypeTransformer() => _instance ??= const ReleaseChannelTypeTransformer._(); + + const ReleaseChannelTypeTransformer._(); + + String encode(ReleaseChannel data) => data.value; + + /// Decodes a [dynamic value][data] to a ReleaseChannel. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ReleaseChannel? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'stable': return ReleaseChannel.stable; + case r'releaseCandidate': return ReleaseChannel.releaseCandidate; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReleaseChannelTypeTransformer] instance. + static ReleaseChannelTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/release_event_v1.dart b/mobile/openapi/lib/model/release_event_v1.dart new file mode 100644 index 0000000000..f26ae3e96e --- /dev/null +++ b/mobile/openapi/lib/model/release_event_v1.dart @@ -0,0 +1,133 @@ +// +// 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 ReleaseEventV1 { + /// Returns a new [ReleaseEventV1] instance. + ReleaseEventV1({ + required this.checkedAt, + required this.isAvailable, + required this.releaseVersion, + required this.serverVersion, + required this.type, + }); + + /// When the server last checked for a latest version. As an ISO timestamp + String checkedAt; + + /// Whether a new version is available + bool isAvailable; + + ServerVersionResponseDto releaseVersion; + + ServerVersionResponseDto serverVersion; + + ReleaseType type; + + @override + bool operator ==(Object other) => identical(this, other) || other is ReleaseEventV1 && + other.checkedAt == checkedAt && + other.isAvailable == isAvailable && + other.releaseVersion == releaseVersion && + other.serverVersion == serverVersion && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (checkedAt.hashCode) + + (isAvailable.hashCode) + + (releaseVersion.hashCode) + + (serverVersion.hashCode) + + (type.hashCode); + + @override + String toString() => 'ReleaseEventV1[checkedAt=$checkedAt, isAvailable=$isAvailable, releaseVersion=$releaseVersion, serverVersion=$serverVersion, type=$type]'; + + Map toJson() { + final json = {}; + json[r'checkedAt'] = this.checkedAt; + json[r'isAvailable'] = this.isAvailable; + json[r'releaseVersion'] = this.releaseVersion; + json[r'serverVersion'] = this.serverVersion; + json[r'type'] = this.type; + return json; + } + + /// Returns a new [ReleaseEventV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ReleaseEventV1? fromJson(dynamic value) { + upgradeDto(value, "ReleaseEventV1"); + if (value is Map) { + final json = value.cast(); + + return ReleaseEventV1( + checkedAt: mapValueOfType(json, r'checkedAt')!, + isAvailable: mapValueOfType(json, r'isAvailable')!, + releaseVersion: ServerVersionResponseDto.fromJson(json[r'releaseVersion'])!, + serverVersion: ServerVersionResponseDto.fromJson(json[r'serverVersion'])!, + type: ReleaseType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReleaseEventV1.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 = ReleaseEventV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ReleaseEventV1-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] = ReleaseEventV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'checkedAt', + 'isAvailable', + 'releaseVersion', + 'serverVersion', + 'type', + }; +} + diff --git a/mobile/openapi/lib/model/release_type.dart b/mobile/openapi/lib/model/release_type.dart new file mode 100644 index 0000000000..2d61072286 --- /dev/null +++ b/mobile/openapi/lib/model/release_type.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class ReleaseType { + /// Instantiate a new enum with the provided [value]. + const ReleaseType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const major = ReleaseType._(r'major'); + static const premajor = ReleaseType._(r'premajor'); + static const minor = ReleaseType._(r'minor'); + static const preminor = ReleaseType._(r'preminor'); + static const patch_ = ReleaseType._(r'patch'); + static const prepatch = ReleaseType._(r'prepatch'); + static const prerelease = ReleaseType._(r'prerelease'); + + /// List of all possible values in this [enum][ReleaseType]. + static const values = [ + major, + premajor, + minor, + preminor, + patch_, + prepatch, + prerelease, + ]; + + static ReleaseType? fromJson(dynamic value) => ReleaseTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReleaseType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReleaseType] to String, +/// and [decode] dynamic data back to [ReleaseType]. +class ReleaseTypeTypeTransformer { + factory ReleaseTypeTypeTransformer() => _instance ??= const ReleaseTypeTypeTransformer._(); + + const ReleaseTypeTypeTransformer._(); + + String encode(ReleaseType data) => data.value; + + /// Decodes a [dynamic value][data] to a ReleaseType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ReleaseType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'major': return ReleaseType.major; + case r'premajor': return ReleaseType.premajor; + case r'minor': return ReleaseType.minor; + case r'preminor': return ReleaseType.preminor; + case r'patch': return ReleaseType.patch_; + case r'prepatch': return ReleaseType.prepatch; + case r'prerelease': return ReleaseType.prerelease; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReleaseTypeTypeTransformer] instance. + static ReleaseTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 79494b74eb..9b75ef2b32 100644 --- a/mobile/openapi/lib/model/server_features_dto.dart +++ b/mobile/openapi/lib/model/server_features_dto.dart @@ -23,6 +23,7 @@ class ServerFeaturesDto { required this.oauthAutoLaunch, required this.ocr, required this.passwordLogin, + required this.realtimeTranscoding, required this.reverseGeocoding, required this.search, required this.sidecar, @@ -60,6 +61,9 @@ class ServerFeaturesDto { /// Whether password login is enabled bool passwordLogin; + /// Whether real-time transcoding is enabled + bool realtimeTranscoding; + /// Whether reverse geocoding is enabled bool reverseGeocoding; @@ -87,6 +91,7 @@ class ServerFeaturesDto { other.oauthAutoLaunch == oauthAutoLaunch && other.ocr == ocr && other.passwordLogin == passwordLogin && + other.realtimeTranscoding == realtimeTranscoding && other.reverseGeocoding == reverseGeocoding && other.search == search && other.sidecar == sidecar && @@ -106,6 +111,7 @@ class ServerFeaturesDto { (oauthAutoLaunch.hashCode) + (ocr.hashCode) + (passwordLogin.hashCode) + + (realtimeTranscoding.hashCode) + (reverseGeocoding.hashCode) + (search.hashCode) + (sidecar.hashCode) + @@ -113,7 +119,7 @@ class ServerFeaturesDto { (trash.hashCode); @override - String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, ocr=$ocr, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]'; + String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, ocr=$ocr, passwordLogin=$passwordLogin, realtimeTranscoding=$realtimeTranscoding, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]'; Map toJson() { final json = {}; @@ -127,6 +133,7 @@ class ServerFeaturesDto { json[r'oauthAutoLaunch'] = this.oauthAutoLaunch; json[r'ocr'] = this.ocr; json[r'passwordLogin'] = this.passwordLogin; + json[r'realtimeTranscoding'] = this.realtimeTranscoding; json[r'reverseGeocoding'] = this.reverseGeocoding; json[r'search'] = this.search; json[r'sidecar'] = this.sidecar; @@ -154,6 +161,7 @@ class ServerFeaturesDto { oauthAutoLaunch: mapValueOfType(json, r'oauthAutoLaunch')!, ocr: mapValueOfType(json, r'ocr')!, passwordLogin: mapValueOfType(json, r'passwordLogin')!, + realtimeTranscoding: mapValueOfType(json, r'realtimeTranscoding')!, reverseGeocoding: mapValueOfType(json, r'reverseGeocoding')!, search: mapValueOfType(json, r'search')!, sidecar: mapValueOfType(json, r'sidecar')!, @@ -216,6 +224,7 @@ class ServerFeaturesDto { 'oauthAutoLaunch', 'ocr', 'passwordLogin', + 'realtimeTranscoding', 'reverseGeocoding', 'search', 'sidecar', diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index 60161a7458..eae574f335 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -16,47 +16,61 @@ class ServerVersionResponseDto { required this.major, required this.minor, required this.patch_, + required this.prerelease, }); /// Major version number /// - /// Minimum value: -9007199254740991 + /// Minimum value: 0 /// Maximum value: 9007199254740991 int major; /// Minor version number /// - /// Minimum value: -9007199254740991 + /// Minimum value: 0 /// Maximum value: 9007199254740991 int minor; /// Patch version number /// - /// Minimum value: -9007199254740991 + /// Minimum value: 0 /// Maximum value: 9007199254740991 int patch_; + /// Pre-release version number + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 + int? prerelease; + @override bool operator ==(Object other) => identical(this, other) || other is ServerVersionResponseDto && other.major == major && other.minor == minor && - other.patch_ == patch_; + other.patch_ == patch_ && + other.prerelease == prerelease; @override int get hashCode => // ignore: unnecessary_parenthesis (major.hashCode) + (minor.hashCode) + - (patch_.hashCode); + (patch_.hashCode) + + (prerelease == null ? 0 : prerelease!.hashCode); @override - String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_]'; + String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_, prerelease=$prerelease]'; Map toJson() { final json = {}; json[r'major'] = this.major; json[r'minor'] = this.minor; json[r'patch'] = this.patch_; + if (this.prerelease != null) { + json[r'prerelease'] = this.prerelease; + } else { + // json[r'prerelease'] = null; + } return json; } @@ -72,6 +86,7 @@ class ServerVersionResponseDto { major: mapValueOfType(json, r'major')!, minor: mapValueOfType(json, r'minor')!, patch_: mapValueOfType(json, r'patch')!, + prerelease: mapValueOfType(json, r'prerelease'), ); } return null; @@ -122,6 +137,7 @@ class ServerVersionResponseDto { 'major', 'minor', 'patch', + 'prerelease', }; } diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index ecf2e5da4a..79da8da97f 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -25,6 +25,7 @@ class SystemConfigFFmpegDto { required this.maxBitrate, required this.preferredHwDevice, required this.preset, + required this.realtime, required this.refs, required this.targetAudioCodec, required this.targetResolution, @@ -79,6 +80,8 @@ class SystemConfigFFmpegDto { /// Preset String preset; + SystemConfigFFmpegRealtimeDto realtime; + /// References /// /// Minimum value: 0 @@ -122,6 +125,7 @@ class SystemConfigFFmpegDto { other.maxBitrate == maxBitrate && other.preferredHwDevice == preferredHwDevice && other.preset == preset && + other.realtime == realtime && other.refs == refs && other.targetAudioCodec == targetAudioCodec && other.targetResolution == targetResolution && @@ -147,6 +151,7 @@ class SystemConfigFFmpegDto { (maxBitrate.hashCode) + (preferredHwDevice.hashCode) + (preset.hashCode) + + (realtime.hashCode) + (refs.hashCode) + (targetAudioCodec.hashCode) + (targetResolution.hashCode) + @@ -158,7 +163,7 @@ class SystemConfigFFmpegDto { (twoPass.hashCode); @override - String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedContainers=$acceptedContainers, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]'; + String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedContainers=$acceptedContainers, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, preferredHwDevice=$preferredHwDevice, preset=$preset, realtime=$realtime, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]'; Map toJson() { final json = {}; @@ -174,6 +179,7 @@ class SystemConfigFFmpegDto { json[r'maxBitrate'] = this.maxBitrate; json[r'preferredHwDevice'] = this.preferredHwDevice; json[r'preset'] = this.preset; + json[r'realtime'] = this.realtime; json[r'refs'] = this.refs; json[r'targetAudioCodec'] = this.targetAudioCodec; json[r'targetResolution'] = this.targetResolution; @@ -207,6 +213,7 @@ class SystemConfigFFmpegDto { maxBitrate: mapValueOfType(json, r'maxBitrate')!, preferredHwDevice: mapValueOfType(json, r'preferredHwDevice')!, preset: mapValueOfType(json, r'preset')!, + realtime: SystemConfigFFmpegRealtimeDto.fromJson(json[r'realtime'])!, refs: mapValueOfType(json, r'refs')!, targetAudioCodec: AudioCodec.fromJson(json[r'targetAudioCodec'])!, targetResolution: mapValueOfType(json, r'targetResolution')!, @@ -275,6 +282,7 @@ class SystemConfigFFmpegDto { 'maxBitrate', 'preferredHwDevice', 'preset', + 'realtime', 'refs', 'targetAudioCodec', 'targetResolution', diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_realtime_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_realtime_dto.dart new file mode 100644 index 0000000000..1a8669912e --- /dev/null +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_realtime_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigFFmpegRealtimeDto { + /// Returns a new [SystemConfigFFmpegRealtimeDto] instance. + SystemConfigFFmpegRealtimeDto({ + required this.enabled, + }); + + /// Enable real-time HLS transcoding (alpha) + bool enabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegRealtimeDto && + other.enabled == enabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode); + + @override + String toString() => 'SystemConfigFFmpegRealtimeDto[enabled=$enabled]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + return json; + } + + /// Returns a new [SystemConfigFFmpegRealtimeDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigFFmpegRealtimeDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigFFmpegRealtimeDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigFFmpegRealtimeDto( + enabled: mapValueOfType(json, r'enabled')!, + ); + } + 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 = SystemConfigFFmpegRealtimeDto.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 = SystemConfigFFmpegRealtimeDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigFFmpegRealtimeDto-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] = SystemConfigFFmpegRealtimeDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart index ec2b400dfd..17ae9577e8 100644 --- a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart +++ b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart @@ -13,26 +13,32 @@ part of openapi.api; class SystemConfigNewVersionCheckDto { /// Returns a new [SystemConfigNewVersionCheckDto] instance. SystemConfigNewVersionCheckDto({ + required this.channel, required this.enabled, }); + ReleaseChannel channel; + /// Enabled bool enabled; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigNewVersionCheckDto && + other.channel == channel && other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis + (channel.hashCode) + (enabled.hashCode); @override - String toString() => 'SystemConfigNewVersionCheckDto[enabled=$enabled]'; + String toString() => 'SystemConfigNewVersionCheckDto[channel=$channel, enabled=$enabled]'; Map toJson() { final json = {}; + json[r'channel'] = this.channel; json[r'enabled'] = this.enabled; return json; } @@ -46,6 +52,7 @@ class SystemConfigNewVersionCheckDto { final json = value.cast(); return SystemConfigNewVersionCheckDto( + channel: ReleaseChannel.fromJson(json[r'channel'])!, enabled: mapValueOfType(json, r'enabled')!, ); } @@ -94,6 +101,7 @@ class SystemConfigNewVersionCheckDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'channel', 'enabled', }; } diff --git a/mobile/openapi/lib/model/workflow_trigger.dart b/mobile/openapi/lib/model/workflow_trigger.dart index 47bf95e05e..b56d1b0dba 100644 --- a/mobile/openapi/lib/model/workflow_trigger.dart +++ b/mobile/openapi/lib/model/workflow_trigger.dart @@ -24,11 +24,13 @@ class WorkflowTrigger { String toJson() => value; static const assetCreate = WorkflowTrigger._(r'AssetCreate'); + static const assetMetadataExtraction = WorkflowTrigger._(r'AssetMetadataExtraction'); static const personRecognized = WorkflowTrigger._(r'PersonRecognized'); /// List of all possible values in this [enum][WorkflowTrigger]. static const values = [ assetCreate, + assetMetadataExtraction, personRecognized, ]; @@ -69,6 +71,7 @@ class WorkflowTriggerTypeTransformer { if (data != null) { switch (data) { case r'AssetCreate': return WorkflowTrigger.assetCreate; + case r'AssetMetadataExtraction': return WorkflowTrigger.assetMetadataExtraction; case r'PersonRecognized': return WorkflowTrigger.personRecognized; default: if (!allowNull) { diff --git a/mobile/packages/ui/lib/src/components/form.dart b/mobile/packages/ui/lib/src/components/form.dart index 4d0344ae6b..31e9d01980 100644 --- a/mobile/packages/ui/lib/src/components/form.dart +++ b/mobile/packages/ui/lib/src/components/form.dart @@ -10,9 +10,16 @@ class ImmichFormController extends ChangeNotifier { FutureOr Function()? onSubmit; final formKey = GlobalKey(); + bool _isDisposed = false; bool _isLoading = false; bool get isLoading => _isLoading; + @override + void dispose() { + _isDisposed = true; + super.dispose(); + } + Future submit() async { if (_isLoading) { return; @@ -27,7 +34,9 @@ class ImmichFormController extends ChangeNotifier { await onSubmit?.call(); } finally { _isLoading = false; - notifyListeners(); + if (!_isDisposed) { + notifyListeners(); + } } } } @@ -38,13 +47,7 @@ class ImmichForm extends StatefulWidget { final String? submitText; final IconData? submitIcon; - const ImmichForm({ - super.key, - this.onSubmit, - this.submitText, - this.submitIcon, - required this.builder, - }); + const ImmichForm({super.key, this.onSubmit, this.submitText, this.submitIcon, required this.builder}); @override State createState() => _ImmichFormState(); diff --git a/mobile/packages/ui/lib/src/previews.dart b/mobile/packages/ui/lib/src/previews.dart new file mode 100644 index 0000000000..076bc253c9 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widget_previews.dart'; +import 'package:immich_ui/src/theme.dart'; + +const ColorScheme _lightColorScheme = ColorScheme.light( + primary: Color(0xFF4250AF), + onPrimary: Colors.white, + primaryContainer: Color(0xFFD4D6F0), + onPrimaryContainer: Color(0xFF181E44), + secondary: Color(0xFF737373), + onSecondary: Colors.white, + error: Color(0xFFE53E3E), + onError: Colors.white, + surface: Color(0xFFFAFAFA), + onSurface: Color(0xFF1A1C1E), + surfaceContainerHighest: Color(0xFFE3E4E8), + outline: Color(0xFFD1D3D9), + outlineVariant: Color(0xFFD4D4D4), +); + +const ColorScheme _darkColorScheme = ColorScheme.dark( + primary: Color(0xFFACCBFA), + onPrimary: Color(0xFF0F1433), + primaryContainer: Color(0xFF616D94), + onPrimaryContainer: Color(0xFFD4D6F0), + secondary: Color(0xFFC4C6D0), + onSecondary: Color(0xFF2E3042), + error: Color(0xFFE88080), + onError: Color(0xFF0F1433), + surface: Color(0xFF0A0A0A), + onSurface: Color(0xFFE3E3E6), + surfaceContainerHighest: Color(0xFF262626), + outline: Color(0xFF8E9099), + outlineVariant: Color(0xFF43464F), +); + +PreviewThemeData immichPreviewTheme() => PreviewThemeData( + materialLight: ThemeData(colorScheme: _lightColorScheme, useMaterial3: true), + materialDark: ThemeData(colorScheme: _darkColorScheme, useMaterial3: true), + ); + +Widget immichPreviewWrapper(Widget child) { + return Builder( + builder: (context) => ImmichThemeProvider( + colorScheme: Theme.of(context).colorScheme, + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: Padding( + padding: const EdgeInsets.all(16), + child: Align(alignment: Alignment.topLeft, child: child), + ), + ), + ), + ); +} + +final class ImmichPreview extends Preview { + const ImmichPreview({super.name, super.group, super.size, super.textScaleFactor}) + : super(theme: immichPreviewTheme, wrapper: immichPreviewWrapper); +} diff --git a/mobile/packages/ui/lib/src/previews/close_button.dart b/mobile/packages/ui/lib/src/previews/close_button.dart new file mode 100644 index 0000000000..678d6bed18 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/close_button.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/close_button.dart'; +import 'package:immich_ui/src/previews.dart'; +import 'package:immich_ui/src/types.dart'; + +void _previewNoop() {} + +@ImmichPreview(group: 'CloseButton', name: 'Variants') +Widget previewCloseButtonVariants() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichCloseButton(onPressed: _previewNoop), + ImmichCloseButton(onPressed: _previewNoop, variant: ImmichVariant.filled), + ], + ); + +@ImmichPreview(group: 'CloseButton', name: 'Colors') +Widget previewCloseButtonColors() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichCloseButton(onPressed: _previewNoop), + ImmichCloseButton(onPressed: _previewNoop, color: ImmichColor.secondary), + ], + ); diff --git a/mobile/packages/ui/lib/src/previews/form.dart b/mobile/packages/ui/lib/src/previews/form.dart new file mode 100644 index 0000000000..9e488cfd68 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/form.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/form.dart'; +import 'package:immich_ui/src/components/password_input.dart'; +import 'package:immich_ui/src/components/text_input.dart'; +import 'package:immich_ui/src/constants.dart'; +import 'package:immich_ui/src/previews.dart'; + +@ImmichPreview(group: 'Form', name: 'Login Form') +Widget previewFormLogin() => const _PreviewLoginForm(); + +class _PreviewLoginForm extends StatefulWidget { + const _PreviewLoginForm(); + + @override + State<_PreviewLoginForm> createState() => _PreviewLoginFormState(); +} + +class _PreviewLoginFormState extends State<_PreviewLoginForm> { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + String _result = ''; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ImmichForm( + submitText: 'Login', + submitIcon: Icons.login, + onSubmit: () async { + await Future.delayed(const Duration(seconds: 1)); + if (!mounted) { + return; + } + setState(() { + _result = 'Form submitted!'; + }); + }, + builder: (context, form) => Column( + spacing: ImmichSpacing.sm, + children: [ + ImmichTextInput( + label: 'Email', + controller: _emailController, + keyboardType: TextInputType.emailAddress, + validator: (value) => value?.isEmpty ?? true ? 'Required' : null, + ), + ImmichPasswordInput( + label: 'Password', + controller: _passwordController, + validator: (value) => value?.isEmpty ?? true ? 'Required' : null, + onSubmit: (_) => form.submit(), + ), + ], + ), + ), + if (_result.isNotEmpty) ...[ + const SizedBox(height: 16), + Text(_result, style: const TextStyle(color: Colors.green)), + ], + ], + ); + } +} diff --git a/mobile/packages/ui/lib/src/previews/formatted_text.dart b/mobile/packages/ui/lib/src/previews/formatted_text.dart new file mode 100644 index 0000000000..9ef3b4b851 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/formatted_text.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/formatted_text.dart'; +import 'package:immich_ui/src/previews.dart'; + +@ImmichPreview(group: 'FormattedText', name: 'Bold') +Widget previewFormattedTextBold() => const ImmichFormattedText('This is bold text.'); + +@ImmichPreview(group: 'FormattedText', name: 'Links') +Widget previewFormattedTextLinks() => const _PreviewFormattedTextLinks(); + +@ImmichPreview(group: 'FormattedText', name: 'Mixed Content') +Widget previewFormattedTextMixed() => const _PreviewFormattedTextMixed(); + +class _PreviewFormattedTextLinks extends StatelessWidget { + const _PreviewFormattedTextLinks(); + + @override + Widget build(BuildContext context) { + return ImmichFormattedText( + 'Read the documentation or visit GitHub.', + spanBuilder: (tag) => FormattedSpan( + onTap: switch (tag) { + 'docs-link' => + () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Docs link clicked!'))), + 'github-link' => + () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('GitHub link clicked!'))), + _ => null, + }, + ), + ); + } +} + +class _PreviewFormattedTextMixed extends StatelessWidget { + const _PreviewFormattedTextMixed(); + + @override + Widget build(BuildContext context) { + return ImmichFormattedText( + 'You can use bold text and links together.', + spanBuilder: (tag) => switch (tag) { + 'b' => const FormattedSpan(style: TextStyle(fontWeight: FontWeight.bold)), + _ => FormattedSpan( + onTap: () => + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Link clicked!'))), + ), + }, + ); + } +} diff --git a/mobile/packages/ui/lib/src/previews/icon_button.dart b/mobile/packages/ui/lib/src/previews/icon_button.dart new file mode 100644 index 0000000000..6a0196bc81 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/icon_button.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/icon_button.dart'; +import 'package:immich_ui/src/previews.dart'; +import 'package:immich_ui/src/types.dart'; + +void _previewNoop() {} + +@ImmichPreview(group: 'IconButton', name: 'Variants') +Widget previewIconButtonVariants() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichIconButton(icon: Icons.add, onPressed: _previewNoop), + ImmichIconButton(icon: Icons.edit, onPressed: _previewNoop, variant: ImmichVariant.ghost), + ], + ); + +@ImmichPreview(group: 'IconButton', name: 'Colors') +Widget previewIconButtonColors() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichIconButton(icon: Icons.favorite, onPressed: _previewNoop), + ImmichIconButton(icon: Icons.delete, onPressed: _previewNoop, color: ImmichColor.secondary), + ], + ); + +@ImmichPreview(group: 'IconButton', name: 'Disabled') +Widget previewIconButtonDisabled() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichIconButton(icon: Icons.settings, onPressed: _previewNoop, disabled: true), + ImmichIconButton( + icon: Icons.settings, + onPressed: _previewNoop, + disabled: true, + variant: ImmichVariant.ghost, + ), + ], + ); diff --git a/mobile/packages/ui/lib/src/previews/password_input.dart b/mobile/packages/ui/lib/src/previews/password_input.dart new file mode 100644 index 0000000000..72bd9cbfc5 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/password_input.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/password_input.dart'; +import 'package:immich_ui/src/previews.dart'; + +@ImmichPreview(group: 'PasswordInput', name: 'With Validator') +Widget previewPasswordInput() => ImmichPasswordInput( + label: 'Password', + hintText: 'Enter your password', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + }, + ); diff --git a/mobile/packages/ui/lib/src/previews/text_button.dart b/mobile/packages/ui/lib/src/previews/text_button.dart new file mode 100644 index 0000000000..46c689bbae --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/text_button.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/text_button.dart'; +import 'package:immich_ui/src/previews.dart'; +import 'package:immich_ui/src/types.dart'; + +void _previewNoop() {} + +@ImmichPreview(group: 'TextButton', name: 'Variants') +Widget previewTextButtonVariants() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton(onPressed: _previewNoop, labelText: 'Filled', expanded: false), + ImmichTextButton(onPressed: _previewNoop, labelText: 'Ghost', variant: ImmichVariant.ghost, expanded: false), + ], +); + +@ImmichPreview(group: 'TextButton', name: 'Colors') +Widget previewTextButtonColors() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton(onPressed: _previewNoop, labelText: 'Primary', expanded: false), + ImmichTextButton(onPressed: _previewNoop, labelText: 'Secondary', color: ImmichColor.secondary, expanded: false), + ], +); + +@ImmichPreview(group: 'TextButton', name: 'With Icons') +Widget previewTextButtonWithIcons() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton(onPressed: _previewNoop, labelText: 'With Icon', icon: Icons.add, expanded: false), + ImmichTextButton( + onPressed: _previewNoop, + labelText: 'Download', + icon: Icons.download, + variant: ImmichVariant.ghost, + expanded: false, + ), + ], +); + +@ImmichPreview(group: 'TextButton', name: 'Loading') +Widget previewTextButtonLoading() => const _PreviewLoadingDemo(); + +@ImmichPreview(group: 'TextButton', name: 'Disabled') +Widget previewTextButtonDisabled() => const Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton(onPressed: _previewNoop, labelText: 'Disabled', disabled: true, expanded: false), + ImmichTextButton( + onPressed: _previewNoop, + labelText: 'Disabled Ghost', + variant: ImmichVariant.ghost, + disabled: true, + expanded: false, + ), + ], +); + +class _PreviewLoadingDemo extends StatefulWidget { + const _PreviewLoadingDemo(); + + @override + State<_PreviewLoadingDemo> createState() => _PreviewLoadingDemoState(); +} + +class _PreviewLoadingDemoState extends State<_PreviewLoadingDemo> { + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return ImmichTextButton( + onPressed: () async { + setState(() => _isLoading = true); + await Future.delayed(const Duration(seconds: 2)); + if (mounted) { + setState(() => _isLoading = false); + } + }, + labelText: _isLoading ? 'Loading...' : 'Click Me', + loading: _isLoading, + expanded: false, + ); + } +} diff --git a/mobile/packages/ui/lib/src/previews/text_input.dart b/mobile/packages/ui/lib/src/previews/text_input.dart new file mode 100644 index 0000000000..fab58c8cc4 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/text_input.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/text_input.dart'; +import 'package:immich_ui/src/previews.dart'; + +@ImmichPreview(group: 'TextInput', name: 'Basic') +Widget previewTextInputBasic() => const _PreviewTextInputBasic(); + +@ImmichPreview(group: 'TextInput', name: 'With Validator') +Widget previewTextInputValidator() => const _PreviewTextInputValidator(); + +class _PreviewTextInputBasic extends StatefulWidget { + const _PreviewTextInputBasic(); + + @override + State<_PreviewTextInputBasic> createState() => _PreviewTextInputBasicState(); +} + +class _PreviewTextInputBasicState extends State<_PreviewTextInputBasic> { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ImmichTextInput( + label: 'Email', + hintText: 'Enter your email', + controller: _controller, + keyboardType: TextInputType.emailAddress, + ); + } +} + +class _PreviewTextInputValidator extends StatefulWidget { + const _PreviewTextInputValidator(); + + @override + State<_PreviewTextInputValidator> createState() => _PreviewTextInputValidatorState(); +} + +class _PreviewTextInputValidatorState extends State<_PreviewTextInputValidator> { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ImmichTextInput( + label: 'Username', + controller: _controller, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Username is required'; + } + if (value.length < 3) { + return 'Username must be at least 3 characters'; + } + return null; + }, + ); + } +} diff --git a/mobile/packages/ui/lib/src/previews/url_input.dart b/mobile/packages/ui/lib/src/previews/url_input.dart new file mode 100644 index 0000000000..6819ac5796 --- /dev/null +++ b/mobile/packages/ui/lib/src/previews/url_input.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/url_input.dart'; +import 'package:immich_ui/src/previews.dart'; + +@ImmichPreview(group: 'URLInput', name: 'Basic') +Widget previewUrlInput() => const _PreviewUrlInput(); + +class _PreviewUrlInput extends StatefulWidget { + const _PreviewUrlInput(); + + @override + State<_PreviewUrlInput> createState() => _PreviewUrlInputState(); +} + +class _PreviewUrlInputState extends State<_PreviewUrlInput> { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ImmichURLInput(label: 'Server URL', hintText: 'https://demo.immich.com', controller: _controller); + } +} diff --git a/mobile/packages/ui/pubspec.lock b/mobile/packages/ui/pubspec.lock index 294c46e424..9d11b49253 100644 --- a/mobile/packages/ui/pubspec.lock +++ b/mobile/packages/ui/pubspec.lock @@ -185,5 +185,5 @@ packages: source: hosted version: "15.2.0" sdks: - dart: ">=3.11.0 <4.0.0" + dart: ">=3.12.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/mobile/packages/ui/pubspec.yaml b/mobile/packages/ui/pubspec.yaml index de50e0a429..1f44694ace 100644 --- a/mobile/packages/ui/pubspec.yaml +++ b/mobile/packages/ui/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_ui publish_to: none environment: - sdk: '>=3.11.0 <4.0.0' + sdk: '>=3.12.0 <4.0.0' dependencies: flutter: diff --git a/mobile/packages/ui/showcase/.gitignore b/mobile/packages/ui/showcase/.gitignore deleted file mode 100644 index b285cd608b..0000000000 --- a/mobile/packages/ui/showcase/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Build artifacts -build/ - -# Test cache and generated files -.dart_tool/ -.packages -.flutter-plugins -.flutter-plugins-dependencies - -# IDE-specific files -.vscode/ \ No newline at end of file diff --git a/mobile/packages/ui/showcase/.metadata b/mobile/packages/ui/showcase/.metadata deleted file mode 100644 index b95fa4d74e..0000000000 --- a/mobile/packages/ui/showcase/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - - platform: web - create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/mobile/packages/ui/showcase/analysis_options.yaml b/mobile/packages/ui/showcase/analysis_options.yaml deleted file mode 100644 index f9b303465f..0000000000 --- a/mobile/packages/ui/showcase/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:flutter_lints/flutter.yaml diff --git a/mobile/packages/ui/showcase/assets/immich-text-dark.png b/mobile/packages/ui/showcase/assets/immich-text-dark.png deleted file mode 100644 index 215687af8f..0000000000 Binary files a/mobile/packages/ui/showcase/assets/immich-text-dark.png and /dev/null differ diff --git a/mobile/packages/ui/showcase/assets/immich-text-light.png b/mobile/packages/ui/showcase/assets/immich-text-light.png deleted file mode 100644 index 478158d39c..0000000000 Binary files a/mobile/packages/ui/showcase/assets/immich-text-light.png and /dev/null differ diff --git a/mobile/packages/ui/showcase/assets/immich_logo.png b/mobile/packages/ui/showcase/assets/immich_logo.png deleted file mode 100644 index 49fd3ae289..0000000000 Binary files a/mobile/packages/ui/showcase/assets/immich_logo.png and /dev/null differ diff --git a/mobile/packages/ui/showcase/assets/themes/github_dark.json b/mobile/packages/ui/showcase/assets/themes/github_dark.json deleted file mode 100644 index bd4801482e..0000000000 --- a/mobile/packages/ui/showcase/assets/themes/github_dark.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "name": "GitHub Dark", - "settings": [ - { - "settings": { - "foreground": "#e1e4e8", - "background": "#24292e" - } - }, - { - "scope": [ - "comment", - "punctuation.definition.comment", - "string.comment" - ], - "settings": { - "foreground": "#6a737d" - } - }, - { - "scope": [ - "constant", - "entity.name.constant", - "variable.other.constant", - "variable.other.enummember", - "variable.language" - ], - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": [ - "entity", - "entity.name" - ], - "settings": { - "foreground": "#b392f0" - } - }, - { - "scope": "variable.parameter.function", - "settings": { - "foreground": "#e1e4e8" - } - }, - { - "scope": "entity.name.tag", - "settings": { - "foreground": "#85e89d" - } - }, - { - "scope": "keyword", - "settings": { - "foreground": "#f97583" - } - }, - { - "scope": [ - "storage", - "storage.type" - ], - "settings": { - "foreground": "#f97583" - } - }, - { - "scope": [ - "storage.modifier.package", - "storage.modifier.import", - "storage.type.java" - ], - "settings": { - "foreground": "#e1e4e8" - } - }, - { - "scope": [ - "string", - "punctuation.definition.string", - "string punctuation.section.embedded source" - ], - "settings": { - "foreground": "#9ecbff" - } - }, - { - "scope": "support", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": "meta.property-name", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": "variable", - "settings": { - "foreground": "#ffab70" - } - }, - { - "scope": "variable.other", - "settings": { - "foreground": "#e1e4e8" - } - }, - { - "scope": "invalid.broken", - "settings": { - "fontStyle": "italic", - "foreground": "#fdaeb7" - } - }, - { - "scope": "invalid.deprecated", - "settings": { - "fontStyle": "italic", - "foreground": "#fdaeb7" - } - }, - { - "scope": "invalid.illegal", - "settings": { - "fontStyle": "italic", - "foreground": "#fdaeb7" - } - }, - { - "scope": "invalid.unimplemented", - "settings": { - "fontStyle": "italic", - "foreground": "#fdaeb7" - } - }, - { - "scope": "message.error", - "settings": { - "foreground": "#fdaeb7" - } - }, - { - "scope": "string variable", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": [ - "source.regexp", - "string.regexp" - ], - "settings": { - "foreground": "#dbedff" - } - }, - { - "scope": [ - "string.regexp.character-class", - "string.regexp constant.character.escape", - "string.regexp source.ruby.embedded", - "string.regexp string.regexp.arbitrary-repitition" - ], - "settings": { - "foreground": "#dbedff" - } - }, - { - "scope": "string.regexp constant.character.escape", - "settings": { - "fontStyle": "bold", - "foreground": "#85e89d" - } - }, - { - "scope": "support.constant", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": "support.variable", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": "meta.module-reference", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": "punctuation.definition.list.begin.markdown", - "settings": { - "foreground": "#ffab70" - } - }, - { - "scope": [ - "markup.heading", - "markup.heading entity.name" - ], - "settings": { - "fontStyle": "bold", - "foreground": "#79b8ff" - } - }, - { - "scope": "markup.quote", - "settings": { - "foreground": "#85e89d" - } - }, - { - "scope": "markup.italic", - "settings": { - "fontStyle": "italic", - "foreground": "#e1e4e8" - } - }, - { - "scope": "markup.bold", - "settings": { - "fontStyle": "bold", - "foreground": "#e1e4e8" - } - }, - { - "scope": "markup.underline", - "settings": { - "fontStyle": "underline" - } - }, - { - "scope": "markup.inline.raw", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": [ - "markup.deleted", - "meta.diff.header.from-file", - "punctuation.definition.deleted" - ], - "settings": { - "foreground": "#fdaeb7" - } - }, - { - "scope": [ - "markup.inserted", - "meta.diff.header.to-file", - "punctuation.definition.inserted" - ], - "settings": { - "foreground": "#85e89d" - } - }, - { - "scope": [ - "markup.changed", - "punctuation.definition.changed" - ], - "settings": { - "foreground": "#ffab70" - } - }, - { - "scope": [ - "markup.ignored", - "markup.untracked" - ], - "settings": { - "foreground": "#2f363d" - } - }, - { - "scope": "meta.diff.range", - "settings": { - "fontStyle": "bold", - "foreground": "#b392f0" - } - }, - { - "scope": "meta.diff.header", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": "meta.separator", - "settings": { - "fontStyle": "bold", - "foreground": "#79b8ff" - } - }, - { - "scope": "meta.output", - "settings": { - "foreground": "#79b8ff" - } - }, - { - "scope": [ - "brackethighlighter.tag", - "brackethighlighter.curly", - "brackethighlighter.round", - "brackethighlighter.square", - "brackethighlighter.angle", - "brackethighlighter.quote" - ], - "settings": { - "foreground": "#d1d5da" - } - }, - { - "scope": "brackethighlighter.unmatched", - "settings": { - "foreground": "#fdaeb7" - } - }, - { - "scope": [ - "constant.other.reference.link", - "string.other.link" - ], - "settings": { - "fontStyle": "underline", - "foreground": "#dbedff" - } - } - ] -} diff --git a/mobile/packages/ui/showcase/lib/app_theme.dart b/mobile/packages/ui/showcase/lib/app_theme.dart deleted file mode 100644 index 995bf3c91e..0000000000 --- a/mobile/packages/ui/showcase/lib/app_theme.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppTheme { - // Light theme colors - static const _primary500 = Color(0xFF4250AF); - static const _primary100 = Color(0xFFD4D6F0); - static const _primary900 = Color(0xFF181E44); - static const _danger500 = Color(0xFFE53E3E); - static const _light50 = Color(0xFFFAFAFA); - static const _light300 = Color(0xFFD4D4D4); - static const _light500 = Color(0xFF737373); - - // Dark theme colors - static const _darkPrimary500 = Color(0xFFACCBFA); - static const _darkPrimary300 = Color(0xFF616D94); - static const _darkDanger500 = Color(0xFFE88080); - static const _darkLight50 = Color(0xFF0A0A0A); - static const _darkLight100 = Color(0xFF171717); - static const _darkLight200 = Color(0xFF262626); - - static ThemeData get lightTheme { - return ThemeData( - colorScheme: const ColorScheme.light( - primary: _primary500, - onPrimary: Colors.white, - primaryContainer: _primary100, - onPrimaryContainer: _primary900, - secondary: _light500, - onSecondary: Colors.white, - error: _danger500, - onError: Colors.white, - surface: _light50, - onSurface: Color(0xFF1A1C1E), - surfaceContainerHighest: Color(0xFFE3E4E8), - outline: Color(0xFFD1D3D9), - outlineVariant: _light300, - ), - useMaterial3: true, - fontFamily: 'GoogleSans', - scaffoldBackgroundColor: _light50, - cardTheme: const CardThemeData( - elevation: 0, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - side: BorderSide(color: _light300, width: 1), - ), - ), - appBarTheme: const AppBarTheme( - centerTitle: false, - elevation: 0, - backgroundColor: Colors.white, - surfaceTintColor: Colors.transparent, - foregroundColor: Color(0xFF1A1C1E), - ), - ); - } - - static ThemeData get darkTheme { - return ThemeData( - colorScheme: const ColorScheme.dark( - primary: _darkPrimary500, - onPrimary: Color(0xFF0F1433), - primaryContainer: _darkPrimary300, - onPrimaryContainer: _primary100, - secondary: Color(0xFFC4C6D0), - onSecondary: Color(0xFF2E3042), - error: _darkDanger500, - onError: Color(0xFF0F1433), - surface: _darkLight50, - onSurface: Color(0xFFE3E3E6), - surfaceContainerHighest: _darkLight200, - outline: Color(0xFF8E9099), - outlineVariant: Color(0xFF43464F), - ), - useMaterial3: true, - fontFamily: 'GoogleSans', - scaffoldBackgroundColor: _darkLight50, - cardTheme: const CardThemeData( - elevation: 0, - color: _darkLight100, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - side: BorderSide(color: _darkLight200, width: 1), - ), - ), - appBarTheme: const AppBarTheme( - centerTitle: false, - elevation: 0, - backgroundColor: _darkLight50, - surfaceTintColor: Colors.transparent, - foregroundColor: Color(0xFFE3E3E6), - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/constants.dart b/mobile/packages/ui/showcase/lib/constants.dart deleted file mode 100644 index cfca4cfda9..0000000000 --- a/mobile/packages/ui/showcase/lib/constants.dart +++ /dev/null @@ -1,16 +0,0 @@ -const String appTitle = '@immich/ui'; - -class LayoutConstants { - static const double sidebarWidth = 220.0; - - static const double gridSpacing = 16.0; - static const double gridAspectRatio = 2.5; - - static const double borderRadiusSmall = 6.0; - static const double borderRadiusMedium = 8.0; - static const double borderRadiusLarge = 12.0; - - static const double iconSizeSmall = 16.0; - static const double iconSizeMedium = 18.0; - static const double iconSizeLarge = 20.0; -} diff --git a/mobile/packages/ui/showcase/lib/main.dart b/mobile/packages/ui/showcase/lib/main.dart deleted file mode 100644 index 6cd2df4fe5..0000000000 --- a/mobile/packages/ui/showcase/lib/main.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/app_theme.dart'; -import 'package:showcase/constants.dart'; -import 'package:showcase/router.dart'; -import 'package:showcase/widgets/example_card.dart'; - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await initializeCodeHighlighter(); - runApp(const ShowcaseApp()); -} - -class ShowcaseApp extends StatefulWidget { - const ShowcaseApp({super.key}); - - @override - State createState() => _ShowcaseAppState(); -} - -class _ShowcaseAppState extends State { - ThemeMode _themeMode = ThemeMode.light; - late final GoRouter _router; - - @override - void initState() { - super.initState(); - _router = AppRouter.createRouter(_toggleTheme); - } - - void _toggleTheme() { - setState(() { - _themeMode = _themeMode == ThemeMode.light - ? ThemeMode.dark - : ThemeMode.light; - }); - } - - @override - Widget build(BuildContext context) { - return MaterialApp.router( - title: appTitle, - themeMode: _themeMode, - routerConfig: _router, - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - debugShowCheckedModeBanner: false, - builder: (context, child) => ImmichThemeProvider( - colorScheme: Theme.of(context).colorScheme, - child: child!, - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart deleted file mode 100644 index 1bae98e0a4..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class CloseButtonPage extends StatelessWidget { - const CloseButtonPage({super.key}); - - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.closeButton.name, - child: ComponentExamples( - title: 'ImmichCloseButton', - subtitle: 'Pre-configured close button for dialogs and sheets.', - examples: [ - ExampleCard( - title: 'Default & Custom', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - ImmichCloseButton(onPressed: () {}), - ImmichCloseButton( - variant: ImmichVariant.filled, - onPressed: () {}, - ), - ImmichCloseButton( - color: ImmichColor.secondary, - onPressed: () {}, - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart deleted file mode 100644 index 7e36ac7537..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; - -class FormattedTextBoldText extends StatelessWidget { - const FormattedTextBoldText({super.key}); - - @override - Widget build(BuildContext context) { - return ImmichFormattedText('This is bold text.'); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart deleted file mode 100644 index 3910a5117a..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; - -class FormattedTextLinks extends StatelessWidget { - const FormattedTextLinks({super.key}); - - @override - Widget build(BuildContext context) { - return ImmichFormattedText( - 'Read the documentation or visit GitHub.', - spanBuilder: (tag) => FormattedSpan( - onTap: switch (tag) { - 'docs-link' => () => ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Docs link clicked!'))), - 'github-link' => () => ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('GitHub link clicked!'))), - _ => null, - }, - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart deleted file mode 100644 index 3490b1c386..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; - -class FormattedTextMixedContent extends StatelessWidget { - const FormattedTextMixedContent({super.key}); - - @override - Widget build(BuildContext context) { - return ImmichFormattedText( - 'You can use bold text and links together.', - spanBuilder: (tag) => switch (tag) { - 'b' => const FormattedSpan( - style: TextStyle(fontWeight: FontWeight.bold), - ), - _ => FormattedSpan( - onTap: () => ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Link clicked!'))), - ), - }, - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/form_page.dart b/mobile/packages/ui/showcase/lib/pages/components/form_page.dart deleted file mode 100644 index f4480026b3..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/form_page.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class FormPage extends StatefulWidget { - const FormPage({super.key}); - - @override - State createState() => _FormPageState(); -} - -class _FormPageState extends State { - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - String _result = ''; - - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.form.name, - child: ComponentExamples( - title: 'ImmichForm', - subtitle: - 'Form container with built-in validation and submit handling.', - examples: [ - ExampleCard( - title: 'Login Form', - preview: Column( - children: [ - ImmichForm( - submitText: 'Login', - submitIcon: Icons.login, - onSubmit: () async { - await Future.delayed(const Duration(seconds: 1)); - setState(() { - _result = 'Form submitted!'; - }); - }, - builder: (context, form) => Column( - spacing: 10, - children: [ - ImmichTextInput( - label: 'Email', - controller: _emailController, - keyboardType: TextInputType.emailAddress, - validator: (value) => - value?.isEmpty ?? true ? 'Required' : null, - ), - ImmichPasswordInput( - label: 'Password', - controller: _passwordController, - validator: (value) => - value?.isEmpty ?? true ? 'Required' : null, - onSubmit: (_) => form.submit(), - ), - ], - ), - ), - if (_result.isNotEmpty) ...[ - const SizedBox(height: 16), - Text(_result, style: const TextStyle(color: Colors.green)), - ], - ], - ), - ), - ], - ), - ); - } - - @override - void dispose() { - _emailController.dispose(); - _passwordController.dispose(); - super.dispose(); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart b/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart deleted file mode 100644 index b827e0340b..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:showcase/pages/components/examples/formatted_text_bold_text.dart'; -import 'package:showcase/pages/components/examples/formatted_text_links.dart'; -import 'package:showcase/pages/components/examples/formatted_text_mixed_tags.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class FormattedTextPage extends StatelessWidget { - const FormattedTextPage({super.key}); - - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.formattedText.name, - child: ComponentExamples( - title: 'ImmichFormattedText', - subtitle: 'Render text with HTML formatting (bold, links).', - examples: [ - ExampleCard( - title: 'Bold Text', - preview: const FormattedTextBoldText(), - code: 'formatted_text_bold_text.dart', - ), - ExampleCard( - title: 'Links', - preview: const FormattedTextLinks(), - code: 'formatted_text_links.dart', - ), - ExampleCard( - title: 'Mixed Content', - preview: const FormattedTextMixedContent(), - code: 'formatted_text_mixed_tags.dart', - ), - ], - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart deleted file mode 100644 index 4418b1de4f..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class IconButtonPage extends StatelessWidget { - const IconButtonPage({super.key}); - - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.iconButton.name, - child: ComponentExamples( - title: 'ImmichIconButton', - subtitle: 'Icon-only button with customizable styling.', - examples: [ - ExampleCard( - title: 'Variants & Colors', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - ImmichIconButton( - icon: Icons.add, - onPressed: () {}, - variant: ImmichVariant.filled, - ), - ImmichIconButton( - icon: Icons.edit, - onPressed: () {}, - variant: ImmichVariant.ghost, - ), - ImmichIconButton( - icon: Icons.delete, - onPressed: () {}, - color: ImmichColor.secondary, - ), - ImmichIconButton( - icon: Icons.settings, - onPressed: () {}, - disabled: true, - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart b/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart deleted file mode 100644 index 772dd7882f..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class PasswordInputPage extends StatelessWidget { - const PasswordInputPage({super.key}); - - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.passwordInput.name, - child: ComponentExamples( - title: 'ImmichPasswordInput', - subtitle: 'Password field with visibility toggle.', - examples: [ - ExampleCard( - title: 'Password Input', - preview: ImmichPasswordInput( - label: 'Password', - hintText: 'Enter your password', - validator: (value) { - if (value == null || value.isEmpty) { - return 'Password is required'; - } - if (value.length < 8) { - return 'Password must be at least 8 characters'; - } - return null; - }, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart deleted file mode 100644 index 59e5b86294..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class TextButtonPage extends StatefulWidget { - const TextButtonPage({super.key}); - - @override - State createState() => _TextButtonPageState(); -} - -class _TextButtonPageState extends State { - bool _isLoading = false; - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.textButton.name, - child: ComponentExamples( - title: 'ImmichTextButton', - subtitle: - 'A versatile button component with multiple variants and color options.', - examples: [ - ExampleCard( - title: 'Variants', - description: - 'Filled and ghost variants for different visual hierarchy', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - ImmichTextButton( - onPressed: () {}, - labelText: 'Filled', - variant: ImmichVariant.filled, - expanded: false, - ), - ImmichTextButton( - onPressed: () {}, - labelText: 'Ghost', - variant: ImmichVariant.ghost, - expanded: false, - ), - ], - ), - ), - ExampleCard( - title: 'Colors', - description: 'Primary and secondary color options', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - ImmichTextButton( - onPressed: () {}, - labelText: 'Primary', - color: ImmichColor.primary, - expanded: false, - ), - ImmichTextButton( - onPressed: () {}, - labelText: 'Secondary', - color: ImmichColor.secondary, - expanded: false, - ), - ], - ), - ), - ExampleCard( - title: 'With Icons', - description: 'Add leading icons', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - ImmichTextButton( - onPressed: () {}, - labelText: 'With Icon', - icon: Icons.add, - expanded: false, - ), - ImmichTextButton( - onPressed: () {}, - labelText: 'Download', - icon: Icons.download, - variant: ImmichVariant.ghost, - expanded: false, - ), - ], - ), - ), - ExampleCard( - title: 'Loading State', - description: 'Shows loading indicator during async operations', - preview: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ImmichTextButton( - onPressed: () async { - setState(() => _isLoading = true); - await Future.delayed(const Duration(seconds: 2)); - if (mounted) setState(() => _isLoading = false); - }, - labelText: _isLoading ? 'Loading...' : 'Click Me', - loading: _isLoading, - expanded: false, - ), - ], - ), - ), - ExampleCard( - title: 'Disabled State', - description: 'Buttons can be disabled', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - ImmichTextButton( - onPressed: () {}, - labelText: 'Disabled', - disabled: true, - expanded: false, - ), - ImmichTextButton( - onPressed: () {}, - labelText: 'Disabled Ghost', - variant: ImmichVariant.ghost, - disabled: true, - expanded: false, - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart b/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart deleted file mode 100644 index 5a0bfec6cd..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class TextInputPage extends StatefulWidget { - const TextInputPage({super.key}); - - @override - State createState() => _TextInputPageState(); -} - -class _TextInputPageState extends State { - final _controller1 = TextEditingController(); - final _controller2 = TextEditingController(); - - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.textInput.name, - child: ComponentExamples( - title: 'ImmichTextInput', - subtitle: 'Text field with validation support.', - examples: [ - ExampleCard( - title: 'Basic Usage', - preview: Column( - children: [ - ImmichTextInput( - label: 'Email', - hintText: 'Enter your email', - controller: _controller1, - keyboardType: TextInputType.emailAddress, - ), - const SizedBox(height: 16), - ImmichTextInput( - label: 'Username', - controller: _controller2, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Username is required'; - } - if (value.length < 3) { - return 'Username must be at least 3 characters'; - } - return null; - }, - ), - ], - ), - ), - ], - ), - ); - } - - @override - void dispose() { - _controller1.dispose(); - _controller2.dispose(); - super.dispose(); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart b/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart deleted file mode 100644 index 17de02d80a..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart +++ /dev/null @@ -1,396 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class ConstantsPage extends StatefulWidget { - const ConstantsPage({super.key}); - - @override - State createState() => _ConstantsPageState(); -} - -class _ConstantsPageState extends State { - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.constants.name, - child: ComponentExamples( - title: 'Constants', - subtitle: 'Consistent spacing, sizing, and styling constants.', - expand: true, - examples: [ - const ExampleCard( - title: 'Spacing', - description: 'ImmichSpacing (4.0 → 48.0)', - preview: Column( - children: [ - _SpacingBox(label: 'xs', size: ImmichSpacing.xs), - _SpacingBox(label: 'sm', size: ImmichSpacing.sm), - _SpacingBox(label: 'md', size: ImmichSpacing.md), - _SpacingBox(label: 'lg', size: ImmichSpacing.lg), - _SpacingBox(label: 'xl', size: ImmichSpacing.xl), - _SpacingBox(label: 'xxl', size: ImmichSpacing.xxl), - _SpacingBox(label: 'xxxl', size: ImmichSpacing.xxxl), - ], - ), - ), - const ExampleCard( - title: 'Border Radius', - description: 'ImmichRadius (0.0 → 24.0)', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - _RadiusBox(label: 'none', radius: ImmichRadius.none), - _RadiusBox(label: 'xs', radius: ImmichRadius.xs), - _RadiusBox(label: 'sm', radius: ImmichRadius.sm), - _RadiusBox(label: 'md', radius: ImmichRadius.md), - _RadiusBox(label: 'lg', radius: ImmichRadius.lg), - _RadiusBox(label: 'xl', radius: ImmichRadius.xl), - _RadiusBox(label: 'xxl', radius: ImmichRadius.xxl), - ], - ), - ), - const ExampleCard( - title: 'Icon Sizes', - description: 'ImmichIconSize (16.0 → 48.0)', - preview: Wrap( - spacing: 16, - runSpacing: 16, - alignment: WrapAlignment.start, - children: [ - _IconSizeBox(label: 'xs', size: ImmichIconSize.xs), - _IconSizeBox(label: 'sm', size: ImmichIconSize.sm), - _IconSizeBox(label: 'md', size: ImmichIconSize.md), - _IconSizeBox(label: 'lg', size: ImmichIconSize.lg), - _IconSizeBox(label: 'xl', size: ImmichIconSize.xl), - _IconSizeBox(label: 'xxl', size: ImmichIconSize.xxl), - ], - ), - ), - const ExampleCard( - title: 'Text Sizes', - description: 'ImmichTextSize (10.0 → 60.0)', - preview: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Caption', - style: TextStyle(fontSize: ImmichTextSize.caption), - ), - Text('Label', style: TextStyle(fontSize: ImmichTextSize.label)), - Text('Body', style: TextStyle(fontSize: ImmichTextSize.body)), - Text('H6', style: TextStyle(fontSize: ImmichTextSize.h6)), - Text('H5', style: TextStyle(fontSize: ImmichTextSize.h5)), - Text('H4', style: TextStyle(fontSize: ImmichTextSize.h4)), - Text('H3', style: TextStyle(fontSize: ImmichTextSize.h3)), - Text('H2', style: TextStyle(fontSize: ImmichTextSize.h2)), - Text('H1', style: TextStyle(fontSize: ImmichTextSize.h1)), - ], - ), - ), - const ExampleCard( - title: 'Elevation', - description: 'ImmichElevation (0.0 → 16.0)', - preview: Wrap( - spacing: 12, - runSpacing: 12, - children: [ - _ElevationBox(label: 'none', elevation: ImmichElevation.none), - _ElevationBox(label: 'xs', elevation: ImmichElevation.xs), - _ElevationBox(label: 'sm', elevation: ImmichElevation.sm), - _ElevationBox(label: 'md', elevation: ImmichElevation.md), - _ElevationBox(label: 'lg', elevation: ImmichElevation.lg), - _ElevationBox(label: 'xl', elevation: ImmichElevation.xl), - _ElevationBox(label: 'xxl', elevation: ImmichElevation.xxl), - ], - ), - ), - const ExampleCard( - title: 'Border Width', - description: 'ImmichBorderWidth (0.5 → 4.0)', - preview: Column( - children: [ - _BorderBox( - label: 'hairline', - borderWidth: ImmichBorderWidth.hairline, - ), - _BorderBox(label: 'base', borderWidth: ImmichBorderWidth.base), - _BorderBox(label: 'md', borderWidth: ImmichBorderWidth.md), - _BorderBox(label: 'lg', borderWidth: ImmichBorderWidth.lg), - _BorderBox(label: 'xl', borderWidth: ImmichBorderWidth.xl), - ], - ), - ), - const ExampleCard( - title: 'Animation Durations', - description: 'ImmichDuration (100ms → 700ms)', - preview: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - _AnimatedDurationBox( - label: 'Extra Fast', - duration: ImmichDuration.extraFast, - ), - _AnimatedDurationBox( - label: 'Fast', - duration: ImmichDuration.fast, - ), - _AnimatedDurationBox( - label: 'Normal', - duration: ImmichDuration.normal, - ), - _AnimatedDurationBox( - label: 'Slow', - duration: ImmichDuration.slow, - ), - _AnimatedDurationBox( - label: 'Extra Slow', - duration: ImmichDuration.extraSlow, - ), - ], - ), - ), - ], - ), - ); - } -} - -class _SpacingBox extends StatelessWidget { - final String label; - final double size; - - const _SpacingBox({required this.label, required this.size}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - SizedBox( - width: 60, - child: Text( - label, - style: const TextStyle(fontFamily: 'GoogleSansCode'), - ), - ), - Container( - width: size, - height: 24, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text('${size.toStringAsFixed(1)}px'), - ], - ), - ); - } -} - -class _RadiusBox extends StatelessWidget { - final String label; - final double radius; - - const _RadiusBox({required this.label, required this.radius}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(radius), - ), - ), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 12)), - ], - ); - } -} - -class _IconSizeBox extends StatelessWidget { - final String label; - final double size; - - const _IconSizeBox({required this.label, required this.size}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Icon(Icons.palette_rounded, size: size), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 12)), - Text( - '${size.toStringAsFixed(0)}px', - style: const TextStyle(fontSize: 10, color: Colors.grey), - ), - ], - ); - } -} - -class _ElevationBox extends StatelessWidget { - final String label; - final double elevation; - - const _ElevationBox({required this.label, required this.elevation}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Material( - elevation: elevation, - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: Container( - width: 60, - height: 60, - alignment: Alignment.center, - child: Text(label, style: const TextStyle(fontSize: 12)), - ), - ), - const SizedBox(height: 4), - Text( - elevation.toStringAsFixed(1), - style: const TextStyle(fontSize: 10), - ), - ], - ); - } -} - -class _BorderBox extends StatelessWidget { - final String label; - final double borderWidth; - - const _BorderBox({required this.label, required this.borderWidth}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - SizedBox( - width: 80, - child: Text( - label, - style: const TextStyle(fontFamily: 'GoogleSansCode'), - ), - ), - Expanded( - child: Container( - height: 40, - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.primary, - width: borderWidth, - ), - borderRadius: const BorderRadius.all(Radius.circular(4)), - ), - ), - ), - const SizedBox(width: 8), - Text('${borderWidth.toStringAsFixed(1)}px'), - ], - ), - ); - } -} - -class _AnimatedDurationBox extends StatefulWidget { - final String label; - final Duration duration; - - const _AnimatedDurationBox({required this.label, required this.duration}); - - @override - State<_AnimatedDurationBox> createState() => _AnimatedDurationBoxState(); -} - -class _AnimatedDurationBoxState extends State<_AnimatedDurationBox> { - bool _atEnd = false; - bool _isAnimating = false; - - void _playAnimation() async { - if (_isAnimating) return; - setState(() => _isAnimating = true); - setState(() => _atEnd = true); - await Future.delayed(widget.duration); - if (!mounted) return; - setState(() => _atEnd = false); - await Future.delayed(widget.duration); - if (!mounted) return; - setState(() => _isAnimating = false); - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Row( - children: [ - SizedBox( - width: 90, - child: Text( - widget.label, - style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 12), - ), - ), - Expanded( - child: Container( - height: 32, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(6), - ), - child: AnimatedAlign( - duration: widget.duration, - curve: Curves.easeInOut, - alignment: _atEnd ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - width: 60, - height: 28, - margin: const EdgeInsets.symmetric(horizontal: 2), - decoration: BoxDecoration( - color: colorScheme.primary, - borderRadius: BorderRadius.circular(4), - ), - alignment: Alignment.center, - child: Text( - '${widget.duration.inMilliseconds}ms', - style: TextStyle( - fontSize: 11, - color: colorScheme.onPrimary, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ), - ), - const SizedBox(width: 8), - IconButton( - onPressed: _isAnimating ? null : _playAnimation, - icon: Icon( - Icons.play_arrow_rounded, - color: _isAnimating ? colorScheme.outline : colorScheme.primary, - ), - iconSize: 24, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), - ), - ], - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/home_page.dart b/mobile/packages/ui/showcase/lib/pages/home_page.dart deleted file mode 100644 index de7af6c26b..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/home_page.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:showcase/constants.dart'; -import 'package:showcase/routes.dart'; - -class HomePage extends StatelessWidget { - final VoidCallback onThemeToggle; - - const HomePage({super.key, required this.onThemeToggle}); - - @override - Widget build(BuildContext context) { - return Title( - title: appTitle, - color: Theme.of(context).colorScheme.primary, - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), - children: [ - Text( - appTitle, - style: Theme.of(context).textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 12), - Text( - 'A collection of Flutter components that are shared across all Immich projects', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w400, - height: 1.5, - ), - ), - const SizedBox(height: 48), - ...routesByCategory.entries.map((entry) { - if (entry.key == AppRouteCategory.root) { - return const SizedBox.shrink(); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - entry.key.displayName, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 16), - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: LayoutConstants.gridSpacing, - mainAxisSpacing: LayoutConstants.gridSpacing, - childAspectRatio: LayoutConstants.gridAspectRatio, - ), - itemCount: entry.value.length, - itemBuilder: (context, index) { - return _ComponentCard(route: entry.value[index]); - }, - ), - const SizedBox(height: 48), - ], - ); - }), - ], - ), - ); - } -} - -class _ComponentCard extends StatelessWidget { - final AppRoute route; - - const _ComponentCard({required this.route}); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () => context.go(route.path), - borderRadius: const BorderRadius.all(Radius.circular(LayoutConstants.borderRadiusLarge)), - child: Card( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Icon(route.icon, size: 32, color: Theme.of(context).colorScheme.primary), - const SizedBox(height: 16), - Text( - route.name, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - - const SizedBox(height: 8), - Text( - route.description, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.4), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/router.dart b/mobile/packages/ui/showcase/lib/router.dart deleted file mode 100644 index 34393da508..0000000000 --- a/mobile/packages/ui/showcase/lib/router.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:showcase/pages/components/close_button_page.dart'; -import 'package:showcase/pages/components/form_page.dart'; -import 'package:showcase/pages/components/formatted_text_page.dart'; -import 'package:showcase/pages/components/icon_button_page.dart'; -import 'package:showcase/pages/components/password_input_page.dart'; -import 'package:showcase/pages/components/text_button_page.dart'; -import 'package:showcase/pages/components/text_input_page.dart'; -import 'package:showcase/pages/design_system/constants_page.dart'; -import 'package:showcase/pages/home_page.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/shell_layout.dart'; - -class AppRouter { - static GoRouter createRouter(VoidCallback onThemeToggle) { - return GoRouter( - initialLocation: AppRoute.home.path, - routes: [ - ShellRoute( - builder: (context, state, child) => - ShellLayout(onThemeToggle: onThemeToggle, child: child), - routes: AppRoute.values - .map( - (route) => GoRoute( - path: route.path, - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: switch (route) { - AppRoute.home => HomePage(onThemeToggle: onThemeToggle), - AppRoute.textButton => const TextButtonPage(), - AppRoute.iconButton => const IconButtonPage(), - AppRoute.closeButton => const CloseButtonPage(), - AppRoute.textInput => const TextInputPage(), - AppRoute.passwordInput => const PasswordInputPage(), - AppRoute.form => const FormPage(), - AppRoute.formattedText => const FormattedTextPage(), - AppRoute.constants => const ConstantsPage(), - }, - ), - ), - ) - .toList(), - ), - ], - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/routes.dart b/mobile/packages/ui/showcase/lib/routes.dart deleted file mode 100644 index 4feeeafdb6..0000000000 --- a/mobile/packages/ui/showcase/lib/routes.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:flutter/material.dart'; - -enum AppRouteCategory { - root(''), - forms('Forms'), - buttons('Buttons'), - designSystem('Design System'); - - final String displayName; - const AppRouteCategory(this.displayName); -} - -enum AppRoute { - home( - name: 'Home', - description: 'Home page', - path: '/', - category: AppRouteCategory.root, - icon: Icons.home_outlined, - ), - textButton( - name: 'Text Button', - description: 'Versatile button with filled and ghost variants', - path: '/text-button', - category: AppRouteCategory.buttons, - icon: Icons.smart_button_rounded, - ), - iconButton( - name: 'Icon Button', - description: 'Icon-only button with customizable styling', - path: '/icon-button', - category: AppRouteCategory.buttons, - icon: Icons.radio_button_unchecked_rounded, - ), - closeButton( - name: 'Close Button', - description: 'Pre-configured close button for dialogs', - path: '/close-button', - category: AppRouteCategory.buttons, - icon: Icons.close_rounded, - ), - textInput( - name: 'Text Input', - description: 'Text field with validation support', - path: '/text-input', - category: AppRouteCategory.forms, - icon: Icons.text_fields_outlined, - ), - passwordInput( - name: 'Password Input', - description: 'Password field with visibility toggle', - path: '/password-input', - category: AppRouteCategory.forms, - icon: Icons.password_outlined, - ), - form( - name: 'Form', - description: 'Form container with built-in validation', - path: '/form', - category: AppRouteCategory.forms, - icon: Icons.description_outlined, - ), - formattedText( - name: 'Formatted Text', - description: 'Render text with HTML formatting', - path: '/formatted-text', - category: AppRouteCategory.forms, - icon: Icons.code_rounded, - ), - constants( - name: 'Constants', - description: 'Spacing, colors, typography, and more', - path: '/constants', - category: AppRouteCategory.designSystem, - icon: Icons.palette_outlined, - ); - - final String name; - final String description; - final String path; - final AppRouteCategory category; - final IconData icon; - - const AppRoute({ - required this.name, - required this.description, - required this.path, - required this.category, - required this.icon, - }); -} - -final routesByCategory = AppRoute.values - .fold>>({}, (map, route) { - map.putIfAbsent(route.category, () => []).add(route); - return map; - }); diff --git a/mobile/packages/ui/showcase/lib/widgets/component_examples.dart b/mobile/packages/ui/showcase/lib/widgets/component_examples.dart deleted file mode 100644 index 21e6516079..0000000000 --- a/mobile/packages/ui/showcase/lib/widgets/component_examples.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; - -class ComponentExamples extends StatelessWidget { - final String title; - final String? subtitle; - final List examples; - final bool expand; - - const ComponentExamples({ - super.key, - required this.title, - this.subtitle, - required this.examples, - this.expand = false, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(10, 24, 24, 24), - child: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: _PageHeader(title: title, subtitle: subtitle), - ), - const SliverPadding(padding: EdgeInsets.only(top: 24)), - if (expand) - SliverList.builder( - itemCount: examples.length, - itemBuilder: (context, index) => examples[index], - ) - else - SliverLayoutBuilder( - builder: (context, constraints) { - return SliverList.builder( - itemCount: examples.length, - itemBuilder: (context, index) => Align( - alignment: Alignment.centerLeft, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: constraints.crossAxisExtent * 0.6, - maxWidth: constraints.crossAxisExtent, - ), - child: IntrinsicWidth(child: examples[index]), - ), - ), - ); - }, - ), - ], - ), - ); - } -} - -class _PageHeader extends StatelessWidget { - final String title; - final String? subtitle; - - const _PageHeader({required this.title, this.subtitle}); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of( - context, - ).textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.bold), - ), - if (subtitle != null) ...[ - const SizedBox(height: 8), - Text( - subtitle!, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/widgets/example_card.dart b/mobile/packages/ui/showcase/lib/widgets/example_card.dart deleted file mode 100644 index fea561afb6..0000000000 --- a/mobile/packages/ui/showcase/lib/widgets/example_card.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:showcase/constants.dart'; -import 'package:syntax_highlight/syntax_highlight.dart'; - -late final Highlighter _codeHighlighter; - -Future initializeCodeHighlighter() async { - await Highlighter.initialize(['dart']); - final darkTheme = await HighlighterTheme.loadFromAssets([ - 'assets/themes/github_dark.json', - ], const TextStyle(color: Color(0xFFe1e4e8))); - - _codeHighlighter = Highlighter(language: 'dart', theme: darkTheme); -} - -class ExampleCard extends StatefulWidget { - final String title; - final String? description; - final Widget preview; - final String? code; - - const ExampleCard({ - super.key, - required this.title, - this.description, - required this.preview, - this.code, - }); - - @override - State createState() => _ExampleCardState(); -} - -class _ExampleCardState extends State { - bool _showPreview = true; - String? code; - - @override - void initState() { - super.initState(); - if (widget.code != null) { - rootBundle - .loadString('lib/pages/components/examples/${widget.code!}') - .then((value) { - setState(() { - code = value; - }); - }); - } - } - - @override - Widget build(BuildContext context) { - return Card( - elevation: 1, - margin: const EdgeInsets.only(bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.title, - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - if (widget.description != null) - Text( - widget.description!, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - if (code != null) ...[ - const SizedBox(width: 16), - Row( - children: [ - _ToggleButton( - icon: Icons.visibility_rounded, - label: 'Preview', - isSelected: _showPreview, - onTap: () => setState(() => _showPreview = true), - ), - const SizedBox(width: 8), - _ToggleButton( - icon: Icons.code_rounded, - label: 'Code', - isSelected: !_showPreview, - onTap: () => setState(() => _showPreview = false), - ), - ], - ), - ], - ], - ), - ), - const Divider(height: 1), - if (_showPreview) - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox(width: double.infinity, child: widget.preview), - ) - else - Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFF24292e), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular( - LayoutConstants.borderRadiusMedium, - ), - bottomRight: Radius.circular( - LayoutConstants.borderRadiusMedium, - ), - ), - ), - child: _CodeCard(code: code!), - ), - ], - ), - ); - } -} - -class _ToggleButton extends StatelessWidget { - final IconData icon; - final String label; - final bool isSelected; - final VoidCallback onTap; - - const _ToggleButton({ - required this.icon, - required this.label, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - borderRadius: const BorderRadius.all(Radius.circular(24)), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.7) - : Theme.of(context).colorScheme.primary, - borderRadius: const BorderRadius.all(Radius.circular(24)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 16, - color: Theme.of(context).colorScheme.onPrimary, - ), - const SizedBox(width: 6), - Text( - label, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - ), - ), - ], - ), - ), - ); - } -} - -class _CodeCard extends StatelessWidget { - final String code; - - const _CodeCard({required this.code}); - - @override - Widget build(BuildContext context) { - final lines = code.split('\n'); - final lineNumberColor = Colors.white.withValues(alpha: 0.4); - - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Padding( - padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: List.generate( - lines.length, - (index) => SizedBox( - height: 20, - child: Text( - '${index + 1}', - style: TextStyle( - fontFamily: 'GoogleSansCode', - fontSize: 13, - color: lineNumberColor, - height: 1.5, - ), - ), - ), - ), - ), - const SizedBox(width: 16), - SelectableText.rich( - _codeHighlighter.highlight(code), - style: const TextStyle( - fontFamily: 'GoogleSansCode', - fontSize: 13, - height: 1.54, - ), - ), - ], - ), - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/widgets/page_title.dart b/mobile/packages/ui/showcase/lib/widgets/page_title.dart deleted file mode 100644 index eae3bf6ffb..0000000000 --- a/mobile/packages/ui/showcase/lib/widgets/page_title.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class PageTitle extends StatelessWidget { - final String title; - final Widget child; - - const PageTitle({super.key, required this.title, required this.child}); - - @override - Widget build(BuildContext context) { - return Title( - title: '$title | @immich/ui', - color: Theme.of(context).colorScheme.primary, - child: child, - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart b/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart deleted file mode 100644 index 8bcb687e75..0000000000 --- a/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:showcase/constants.dart'; -import 'package:showcase/widgets/sidebar_navigation.dart'; - -class ShellLayout extends StatelessWidget { - final Widget child; - final VoidCallback onThemeToggle; - - const ShellLayout({ - super.key, - required this.child, - required this.onThemeToggle, - }); - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - appBar: AppBar( - title: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset('assets/immich_logo.png', height: 32, width: 32), - const SizedBox(width: 8), - Image.asset( - isDark - ? 'assets/immich-text-dark.png' - : 'assets/immich-text-light.png', - height: 24, - filterQuality: FilterQuality.none, - isAntiAlias: true, - ), - ], - ), - actions: [ - IconButton( - icon: Icon( - isDark ? Icons.light_mode_outlined : Icons.dark_mode_outlined, - size: LayoutConstants.iconSizeLarge, - ), - onPressed: onThemeToggle, - tooltip: 'Toggle theme', - ), - ], - shape: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1), - ), - ), - body: Row( - children: [ - const SidebarNavigation(), - const VerticalDivider(), - Expanded(child: child), - ], - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart b/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart deleted file mode 100644 index 10eba170e6..0000000000 --- a/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:showcase/constants.dart'; -import 'package:showcase/routes.dart'; - -class SidebarNavigation extends StatelessWidget { - const SidebarNavigation({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - width: LayoutConstants.sidebarWidth, - decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), - children: [ - ...routesByCategory.entries.expand((entry) { - final category = entry.key; - final routes = entry.value; - return [ - if (category != AppRouteCategory.root) _CategoryHeader(category), - ...routes.map((route) => _NavItem(route)), - const SizedBox(height: 24), - ]; - }), - ], - ), - ); - } -} - -class _CategoryHeader extends StatelessWidget { - final AppRouteCategory category; - - const _CategoryHeader(this.category); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8), - child: Text( - category.displayName, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), - ), - ); - } -} - -class _NavItem extends StatelessWidget { - final AppRoute route; - - const _NavItem(this.route); - - @override - Widget build(BuildContext context) { - final currentRoute = GoRouterState.of(context).uri.toString(); - final isSelected = currentRoute == route.path; - final isDark = Theme.of(context).brightness == Brightness.dark; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - context.go(route.path); - }, - borderRadius: BorderRadius.circular( - LayoutConstants.borderRadiusMedium, - ), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: isSelected - ? (isDark - ? Colors.white.withValues(alpha: 0.1) - : Theme.of( - context, - ).colorScheme.primaryContainer.withValues(alpha: 0.5)) - : Colors.transparent, - borderRadius: BorderRadius.circular( - LayoutConstants.borderRadiusMedium, - ), - ), - child: Row( - children: [ - Icon( - route.icon, - size: 20, - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 16), - Expanded( - child: Text( - route.name, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/pubspec.lock b/mobile/packages/ui/showcase/pubspec.lock deleted file mode 100644 index d375d4b05b..0000000000 --- a/mobile/packages/ui/showcase/pubspec.lock +++ /dev/null @@ -1,377 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" - source: hosted - version: "2.13.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - device_info_plus: - dependency: transitive - description: - name: device_info_plus - sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" - url: "https://pub.dev" - source: hosted - version: "11.5.0" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.dev" - source: hosted - version: "7.0.3" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f" - url: "https://pub.dev" - source: hosted - version: "17.2.3" - immich_ui: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.0.0" - irondash_engine_context: - dependency: transitive - description: - name: irondash_engine_context - sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7" - url: "https://pub.dev" - source: hosted - version: "0.5.5" - irondash_message_channel: - dependency: transitive - description: - name: irondash_message_channel - sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 - url: "https://pub.dev" - source: hosted - version: "0.7.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" - source: hosted - version: "0.12.19" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" - url: "https://pub.dev" - source: hosted - version: "1.18.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - pixel_snap: - dependency: transitive - description: - name: pixel_snap - sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" - url: "https://pub.dev" - source: hosted - version: "0.1.5" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - super_clipboard: - dependency: transitive - description: - name: super_clipboard - sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16 - url: "https://pub.dev" - source: hosted - version: "0.9.1" - super_native_extensions: - dependency: transitive - description: - name: super_native_extensions - sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 - url: "https://pub.dev" - source: hosted - version: "0.9.1" - syntax_highlight: - dependency: "direct main" - description: - name: syntax_highlight - sha256: "4d3ba40658cadba6ba55d697f29f00b43538ebb6eb4a0ca0e895c568eaced138" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" - url: "https://pub.dev" - source: hosted - version: "0.7.11" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" - source: hosted - version: "4.5.3" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" - source: hosted - version: "15.2.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" - url: "https://pub.dev" - source: hosted - version: "2.1.0" -sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.35.0" diff --git a/mobile/packages/ui/showcase/pubspec.yaml b/mobile/packages/ui/showcase/pubspec.yaml deleted file mode 100644 index 6353600ce3..0000000000 --- a/mobile/packages/ui/showcase/pubspec.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: showcase -publish_to: 'none' - -version: 1.0.0+1 - -environment: - sdk: ^3.11.0 - -dependencies: - flutter: - sdk: flutter - immich_ui: - path: ../ - go_router: ^17.2.1 - syntax_highlight: ^0.5.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^6.0.0 - -flutter: - uses-material-design: true - assets: - - assets/ - - assets/themes/ - - lib/pages/components/examples/ - - fonts: - - family: GoogleSans - fonts: - - asset: ../../../fonts/GoogleSans/GoogleSans-Regular.ttf - - asset: ../../../fonts/GoogleSans/GoogleSans-Italic.ttf - style: italic - - asset: ../../../fonts/GoogleSans/GoogleSans-Medium.ttf - weight: 500 - - asset: ../../../fonts/GoogleSans/GoogleSans-SemiBold.ttf - weight: 600 - - asset: ../../../fonts/GoogleSans/GoogleSans-Bold.ttf - weight: 700 - - family: GoogleSansCode - fonts: - - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-Regular.ttf - - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-Medium.ttf - weight: 500 - - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf - weight: 600 \ No newline at end of file diff --git a/mobile/packages/ui/showcase/web/favicon.ico b/mobile/packages/ui/showcase/web/favicon.ico deleted file mode 100644 index 7ec34e9e53..0000000000 Binary files a/mobile/packages/ui/showcase/web/favicon.ico and /dev/null differ diff --git a/mobile/packages/ui/showcase/web/icons/Icon-maskable-192.png b/mobile/packages/ui/showcase/web/icons/Icon-maskable-192.png deleted file mode 100644 index 49fd3ae289..0000000000 Binary files a/mobile/packages/ui/showcase/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png b/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png deleted file mode 100644 index a7220554bc..0000000000 Binary files a/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/mobile/packages/ui/showcase/web/icons/apple-icon-180.png b/mobile/packages/ui/showcase/web/icons/apple-icon-180.png deleted file mode 100644 index 4e642631a3..0000000000 Binary files a/mobile/packages/ui/showcase/web/icons/apple-icon-180.png and /dev/null differ diff --git a/mobile/packages/ui/showcase/web/index.html b/mobile/packages/ui/showcase/web/index.html deleted file mode 100644 index abf42ad1fd..0000000000 --- a/mobile/packages/ui/showcase/web/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - @immich/ui - - - - - - diff --git a/mobile/packages/ui/showcase/web/manifest.json b/mobile/packages/ui/showcase/web/manifest.json deleted file mode 100644 index 25b44bd1ae..0000000000 --- a/mobile/packages/ui/showcase/web/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@immich/ui Showcase", - "short_name": "@immich/ui", - "start_url": ".", - "display": "standalone", - "background_color": "#FCFCFD", - "theme_color": "#4250AF", - "description": "Immich UI component library showcase and documentation", - "orientation": "landscape", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index ee596f449e..6a82d1dce3 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -1,9 +1,9 @@ import 'package:collection/collection.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/config/system_config.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:logging/logging.dart'; @@ -29,23 +29,23 @@ final _kWarnLog = LogMessage( void main() { late LogService sut; late LogRepository mockLogRepo; - late MockMetadataRepository mockMetadataRepository; + late MockSettingsRepository mockSettingsRepository; setUp(() async { mockLogRepo = MockLogRepository(); - mockMetadataRepository = MockMetadataRepository(); + mockSettingsRepository = MockSettingsRepository(); registerFallbackValue(_kInfoLog); registerFallbackValue(LogLevel.info); when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {}); - when(() => mockMetadataRepository.systemConfig).thenReturn(const SystemConfig(logLevel: LogLevel.fine)); - when(() => mockMetadataRepository.write(MetadataKey.logLevel, any())).thenAnswer((_) async {}); + when(() => mockSettingsRepository.appConfig).thenReturn(const AppConfig(logLevel: LogLevel.fine)); + when(() => mockSettingsRepository.write(SettingsKey.logLevel, any())).thenAnswer((_) async {}); when(() => mockLogRepo.getAll()).thenAnswer((_) async => []); when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true); when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true); - sut = await LogService.create(logRepository: mockLogRepo, metadataRepository: mockMetadataRepository); + sut = await LogService.create(logRepository: mockLogRepo, settingsRepository: mockSettingsRepository); }); tearDown(() async { @@ -59,7 +59,7 @@ void main() { }); test('Sets log level based on the metadata repository', () { - verify(() => mockMetadataRepository.systemConfig).called(1); + verify(() => mockSettingsRepository.appConfig).called(1); expect(Logger.root.level, Level.FINE); }); }); @@ -71,7 +71,7 @@ void main() { test('Updates the log level via metadata repository', () { final captured = verify( - () => mockMetadataRepository.write(MetadataKey.logLevel, captureAny()), + () => mockSettingsRepository.write(SettingsKey.logLevel, captureAny()), ).captured.firstOrNull; expect(captured, LogLevel.shout); }); @@ -86,7 +86,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: true, ); @@ -104,7 +104,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: true, ); @@ -125,7 +125,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: false, ); @@ -159,7 +159,7 @@ void main() { TestUtils.fakeAsync((time) async { sut = await LogService.create( logRepository: mockLogRepo, - metadataRepository: mockMetadataRepository, + settingsRepository: mockSettingsRepository, shouldBuffer: true, ); diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index ef29997e0b..80272d9310 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -116,7 +116,7 @@ void main() { when(() => mockApi.serverInfoApi).thenReturn(mockServerApi); when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0, prerelease: null)); when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler); @@ -559,7 +559,7 @@ void main() { await Store.put(StoreKey.syncMigrationStatus, "[]"); when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null)); await sut.sync(); @@ -587,7 +587,7 @@ void main() { await Store.put(StoreKey.syncMigrationStatus, "[]"); when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null)); await sut.sync(); verifyInOrder([ @@ -617,7 +617,7 @@ void main() { when( () => mockServerApi.getServerVersion(), - ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1)); + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null)); await sut.sync(); diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index a1bae8f6dd..c5d57e9a4b 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -30,6 +30,7 @@ import 'schema_v23.dart' as v23; import 'schema_v24.dart' as v24; import 'schema_v25.dart' as v25; import 'schema_v26.dart' as v26; +import 'schema_v27.dart' as v27; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -87,6 +88,8 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v25.DatabaseAtV25(db); case 26: return v26.DatabaseAtV26(db); + case 27: + return v27.DatabaseAtV27(db); default: throw MissingSchemaException(version, versions); } @@ -119,5 +122,6 @@ class GeneratedHelper implements SchemaInstantiationHelper { 24, 25, 26, + 27, ]; } diff --git a/mobile/test/drift/main/generated/schema_v27.dart b/mobile/test/drift/main/generated/schema_v27.dart new file mode 100644 index 0000000000..2b02946175 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v27.dart @@ -0,0 +1,9384 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_profile_image IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final int hasProfileImage; + final String profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + int? hasProfileImage, + String? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn durationMs = GeneratedColumn( + 'duration_ms', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn localDateTime = GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn uploadedAt = GeneratedColumn( + 'uploaded_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_edited IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + uploadedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationMs: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_ms'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}deleted_at'], + ), + uploadedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}uploaded_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final String createdAt; + final String updatedAt; + final int? width; + final int? height; + final int? durationMs; + final String id; + final String checksum; + final int isFavorite; + final String ownerId; + final String? localDateTime; + final String? thumbHash; + final String? deletedAt; + final String? uploadedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final int isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationMs, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.uploadedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationMs != null) { + map['duration_ms'] = Variable(durationMs); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || uploadedAt != null) { + map['uploaded_at'] = Variable(uploadedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationMs: serializer.fromJson(json['durationMs']), + 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']), + uploadedAt: serializer.fromJson(json['uploadedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationMs': serializer.toJson(durationMs), + '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), + 'uploadedAt': serializer.toJson(uploadedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + String? createdAt, + String? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationMs = const Value.absent(), + String? id, + String? checksum, + int? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value uploadedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + int? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationMs: durationMs.present ? durationMs.value : this.durationMs, + 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, + uploadedAt: uploadedAt.present ? uploadedAt.value : this.uploadedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationMs: data.durationMs.present + ? data.durationMs.value + : this.durationMs, + 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, + uploadedAt: data.uploadedAt.present + ? data.uploadedAt.value + : this.uploadedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('uploadedAt: $uploadedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + uploadedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationMs == this.durationMs && + 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.uploadedAt == this.uploadedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationMs; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value uploadedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.uploadedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.uploadedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationMs, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? uploadedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationMs != null) 'duration_ms': durationMs, + 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 (uploadedAt != null) 'uploaded_at': uploadedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationMs, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? uploadedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationMs: durationMs ?? this.durationMs, + 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, + uploadedAt: uploadedAt ?? this.uploadedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationMs.present) { + map['duration_ms'] = Variable(durationMs.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (uploadedAt.present) { + map['uploaded_at'] = Variable(uploadedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('uploadedAt: $uploadedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final String createdAt; + final String updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + String? createdAt, + String? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn durationMs = GeneratedColumn( + 'duration_ms', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn adjustmentTime = GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn playbackStyle = GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + playbackStyle, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationMs: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_ms'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + playbackStyle: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final String createdAt; + final String updatedAt; + final int? width; + final int? height; + final int? durationMs; + final String id; + final String? checksum; + final int isFavorite; + final int orientation; + final String? iCloudId; + final String? adjustmentTime; + final double? latitude; + final double? longitude; + final int playbackStyle; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationMs, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + required this.playbackStyle, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationMs != null) { + map['duration_ms'] = Variable(durationMs); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + map['playback_style'] = Variable(playbackStyle); + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationMs: serializer.fromJson(json['durationMs']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + playbackStyle: serializer.fromJson(json['playbackStyle']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationMs': serializer.toJson(durationMs), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'playbackStyle': serializer.toJson(playbackStyle), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + String? createdAt, + String? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationMs = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + int? isFavorite, + int? orientation, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + int? playbackStyle, + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationMs: durationMs.present ? durationMs.value : this.durationMs, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + LocalAssetEntityData copyWithCompanion(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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationMs: data.durationMs.present + ? data.durationMs.value + : this.durationMs, + 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, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + playbackStyle, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationMs == this.durationMs && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.playbackStyle == this.playbackStyle); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationMs; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + final Value playbackStyle; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.playbackStyle = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.playbackStyle = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationMs, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + Expression? playbackStyle, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationMs != null) 'duration_ms': durationMs, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (playbackStyle != null) 'playback_style': playbackStyle, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationMs, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + Value? playbackStyle, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationMs: durationMs ?? this.durationMs, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationMs.present) { + map['duration_ms'] = Variable(durationMs.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (playbackStyle.present) { + map['playback_style'] = Variable(playbackStyle.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'\'', + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'NULL REFERENCES remote_asset_entity(id)ON DELETE SET NULL', + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 1 CHECK (is_activity_enabled IN (0, 1))', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final String createdAt; + final String updatedAt; + final String? thumbnailAssetId; + final int isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + String? createdAt, + String? updatedAt, + Value thumbnailAssetId = const Value.absent(), + int? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (is_ios_shared_album IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'NULL REFERENCES remote_album_entity(id)ON DELETE SET NULL', + ); + late final GeneratedColumn marker = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL CHECK (marker IN (0, 1))', + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String updatedAt; + final int backupSelection; + final int isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final int? marker; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker != null) { + map['marker'] = Variable(marker); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker: serializer.fromJson(json['marker']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker': serializer.toJson(marker), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + String? updatedAt, + int? backupSelection, + int? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker: marker.present ? marker.value : this.marker, + ); + LocalAlbumEntityData copyWithCompanion(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, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + 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('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker == this.marker); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker != null) 'marker': marker, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker: marker ?? this.marker, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker.present) { + map['marker'] = Variable(marker.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES local_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES local_album_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn marker = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL CHECK (marker IN (0, 1))', + ); + @override + List get $columns => [assetId, albumId, marker]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(asset_id, album_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final int? marker; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker != null) { + map['marker'] = Variable(marker); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker: serializer.fromJson(json['marker']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker': serializer.toJson(marker), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker: marker.present ? marker.value : this.marker, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker: data.marker.present ? data.marker.value : this.marker, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker == this.marker); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker != null) 'marker': marker, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker: marker ?? this.marker, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker.present) { + map['marker'] = Variable(marker.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker: $marker') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_admin IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_profile_image IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final int isAdmin; + final int hasProfileImage; + final String profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + int? isAdmin, + int? hasProfileImage, + String? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn value = + GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(user_id, "key")']; + @override + bool get dontWriteConstraints => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final i2.Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + i2.Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required i2.Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (in_timeline IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(shared_by_id, shared_with_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final int inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + int? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn dateTimeOriginal = GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(asset_id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final String? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + 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.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= 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']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= 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), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => 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, + lens: lens.present ? lens.value : this.lens, + 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(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, + lens: data.lens.present ? data.lens.value : this.lens, + 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('lens: $lens, ') + ..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, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is 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.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return 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 (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return 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, + lens: lens ?? this.lens, + 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'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = 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('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_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 = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(asset_id, album_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..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 RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_album_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(album_id, user_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn adjustmentTime = GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(asset_id)']; + @override + bool get dontWriteConstraints => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final String? createdAt; + final String? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_saved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final String createdAt; + final String updatedAt; + final String? deletedAt; + final String ownerId; + final int type; + final String data; + final int isSaved; + final String memoryAt; + final String? seenAt; + final String? showAt; + final String? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + String? createdAt, + String? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + int? isSaved, + String? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required String memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES memory_entity(id)ON DELETE CASCADE', + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const [ + 'PRIMARY KEY(asset_id, memory_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES user_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL CHECK (is_favorite IN (0, 1))', + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL CHECK (is_hidden IN (0, 1))', + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final String createdAt; + final String updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final int isFavorite; + final int isHidden; + final String? color; + final String? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + String? createdAt, + String? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + int? isFavorite, + int? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required int isFavorite, + required int isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES person_entity(id)ON DELETE SET NULL', + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1 CHECK (is_visible IN (0, 1))', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_visible'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + final int isVisible; + final String? deletedAt; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + required this.isVisible, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + isVisible: serializer.fromJson(json['isVisible']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + 'isVisible': serializer.toJson(isVisible), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + int? isVisible, + Value deletedAt = const Value.absent(), + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType && + other.isVisible == this.isVisible && + other.deletedAt == this.deletedAt); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + final Value isVisible; + final Value deletedAt; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + Expression? isVisible, + Expression? deletedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + if (isVisible != null) 'is_visible': isVisible, + if (deletedAt != null) 'deleted_at': deletedAt, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + Value? isVisible, + Value? deletedAt, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt ?? this.deletedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn durationMs = GeneratedColumn( + 'duration_ms', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn playbackStyle = GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + playbackStyle, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationMs: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_ms'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + playbackStyle: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id, album_id)']; + @override + bool get dontWriteConstraints => true; +} + +class TrashedLocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final String createdAt; + final String updatedAt; + final int? width; + final int? height; + final int? durationMs; + final String id; + final String albumId; + final String? checksum; + final int isFavorite; + final int orientation; + final int source; + final int playbackStyle; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationMs, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + required this.playbackStyle, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationMs != null) { + map['duration_ms'] = Variable(durationMs); + } + map['id'] = Variable(id); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + map['playback_style'] = Variable(playbackStyle); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationMs: serializer.fromJson(json['durationMs']), + id: serializer.fromJson(json['id']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + playbackStyle: serializer.fromJson(json['playbackStyle']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationMs': serializer.toJson(durationMs), + 'id': serializer.toJson(id), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + 'playbackStyle': serializer.toJson(playbackStyle), + }; + } + + TrashedLocalAssetEntityData copyWith({ + String? name, + int? type, + String? createdAt, + String? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationMs = const Value.absent(), + String? id, + String? albumId, + Value checksum = const Value.absent(), + int? isFavorite, + int? orientation, + int? source, + int? playbackStyle, + }) => TrashedLocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationMs: durationMs.present ? durationMs.value : this.durationMs, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationMs: data.durationMs.present + ? data.durationMs.value + : this.durationMs, + id: data.id.present ? data.id.value : this.id, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + source: data.source.present ? data.source.value : this.source, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationMs, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + playbackStyle, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationMs == this.durationMs && + other.id == this.id && + other.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source && + other.playbackStyle == this.playbackStyle); +} + +class TrashedLocalAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationMs; + final Value id; + final Value albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + final Value playbackStyle; + const TrashedLocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + this.id = const Value.absent(), + this.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + this.playbackStyle = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationMs = const Value.absent(), + required String id, + required String albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + this.playbackStyle = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationMs, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + Expression? playbackStyle, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationMs != null) 'duration_ms': durationMs, + if (id != null) 'id': id, + if (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + if (playbackStyle != null) 'playback_style': playbackStyle, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationMs, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + Value? playbackStyle, + }) { + return TrashedLocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationMs: durationMs ?? this.durationMs, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationMs.present) { + map['duration_ms'] = Variable(durationMs.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + if (playbackStyle.present) { + map['playback_style'] = Variable(playbackStyle.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationMs: $durationMs, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } +} + +class AssetEditEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetEditEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE', + ); + late final GeneratedColumn action = GeneratedColumn( + 'action', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn parameters = + GeneratedColumn( + 'parameters', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sequence = GeneratedColumn( + 'sequence', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + assetId, + action, + parameters, + sequence, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_edit_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetEditEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetEditEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + action: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}action'], + )!, + parameters: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}parameters'], + )!, + sequence: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sequence'], + )!, + ); + } + + @override + AssetEditEntity createAlias(String alias) { + return AssetEditEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY(id)']; + @override + bool get dontWriteConstraints => true; +} + +class AssetEditEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final int action; + final i2.Uint8List parameters; + final int sequence; + const AssetEditEntityData({ + required this.id, + required this.assetId, + required this.action, + required this.parameters, + required this.sequence, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + map['action'] = Variable(action); + map['parameters'] = Variable(parameters); + map['sequence'] = Variable(sequence); + return map; + } + + factory AssetEditEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetEditEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + action: serializer.fromJson(json['action']), + parameters: serializer.fromJson(json['parameters']), + sequence: serializer.fromJson(json['sequence']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'action': serializer.toJson(action), + 'parameters': serializer.toJson(parameters), + 'sequence': serializer.toJson(sequence), + }; + } + + AssetEditEntityData copyWith({ + String? id, + String? assetId, + int? action, + i2.Uint8List? parameters, + int? sequence, + }) => AssetEditEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + action: action ?? this.action, + parameters: parameters ?? this.parameters, + sequence: sequence ?? this.sequence, + ); + AssetEditEntityData copyWithCompanion(AssetEditEntityCompanion data) { + return AssetEditEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + action: data.action.present ? data.action.value : this.action, + parameters: data.parameters.present + ? data.parameters.value + : this.parameters, + sequence: data.sequence.present ? data.sequence.value : this.sequence, + ); + } + + @override + String toString() { + return (StringBuffer('AssetEditEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('action: $action, ') + ..write('parameters: $parameters, ') + ..write('sequence: $sequence') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + action, + $driftBlobEquality.hash(parameters), + sequence, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetEditEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.action == this.action && + $driftBlobEquality.equals(other.parameters, this.parameters) && + other.sequence == this.sequence); +} + +class AssetEditEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value action; + final Value parameters; + final Value sequence; + const AssetEditEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.action = const Value.absent(), + this.parameters = const Value.absent(), + this.sequence = const Value.absent(), + }); + AssetEditEntityCompanion.insert({ + required String id, + required String assetId, + required int action, + required i2.Uint8List parameters, + required int sequence, + }) : id = Value(id), + assetId = Value(assetId), + action = Value(action), + parameters = Value(parameters), + sequence = Value(sequence); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? action, + Expression? parameters, + Expression? sequence, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (action != null) 'action': action, + if (parameters != null) 'parameters': parameters, + if (sequence != null) 'sequence': sequence, + }); + } + + AssetEditEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? action, + Value? parameters, + Value? sequence, + }) { + return AssetEditEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + action: action ?? this.action, + parameters: parameters ?? this.parameters, + sequence: sequence ?? this.sequence, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (action.present) { + map['action'] = Variable(action.value); + } + if (parameters.present) { + map['parameters'] = Variable(parameters.value); + } + if (sequence.present) { + map['sequence'] = Variable(sequence.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetEditEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('action: $action, ') + ..write('parameters: $parameters, ') + ..write('sequence: $sequence') + ..write(')')) + .toString(); + } +} + +class Settings extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Settings(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP', + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + @override + List get $columns => [key, value, updatedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'settings'; + @override + Set get $primaryKey => {key}; + @override + SettingsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingsData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}value'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at'], + )!, + ); + } + + @override + Settings createAlias(String alias) { + return Settings(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; + @override + List get customConstraints => const ['PRIMARY KEY("key")']; + @override + bool get dontWriteConstraints => true; +} + +class SettingsData extends DataClass implements Insertable { + final String key; + final String value; + final String updatedAt; + const SettingsData({ + required this.key, + required this.value, + required this.updatedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['value'] = Variable(value); + map['updated_at'] = Variable(updatedAt); + return map; + } + + factory SettingsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingsData( + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + SettingsData copyWith({String? key, String? value, String? updatedAt}) => + SettingsData( + key: key ?? this.key, + value: value ?? this.value, + updatedAt: updatedAt ?? this.updatedAt, + ); + SettingsData copyWithCompanion(SettingsCompanion data) { + return SettingsData( + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('SettingsData(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, value, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingsData && + other.key == this.key && + other.value == this.value && + other.updatedAt == this.updatedAt); +} + +class SettingsCompanion extends UpdateCompanion { + final Value key; + final Value value; + final Value updatedAt; + const SettingsCompanion({ + this.key = const Value.absent(), + this.value = const Value.absent(), + this.updatedAt = const Value.absent(), + }); + SettingsCompanion.insert({ + required String key, + required String value, + this.updatedAt = const Value.absent(), + }) : key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? value, + Expression? updatedAt, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (value != null) 'value': value, + if (updatedAt != null) 'updated_at': updatedAt, + }); + } + + SettingsCompanion copyWith({ + Value? key, + Value? value, + Value? updatedAt, + }) { + return SettingsCompanion( + key: key ?? this.key, + value: value ?? this.value, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingsCompanion(') + ..write('key: $key, ') + ..write('value: $value, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV27 extends GeneratedDatabase { + DatabaseAtV27(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAlbumAssetAlbumAsset = Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxStackPrimaryAssetId = Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Index idxRemoteAssetStackId = Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + late final Index idxRemoteAssetOwnerVisibilityDeletedCreated = Index( + 'idx_remote_asset_owner_visibility_deleted_created', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final AssetEditEntity assetEditEntity = AssetEditEntity(this); + late final Settings settings = Settings(this); + late final Index idxPartnerSharedWithId = Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteExifCity = Index( + 'idx_remote_exif_city', + 'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL', + ); + late final Index idxRemoteAlbumAssetAlbumAsset = Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxPersonOwnerId = Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + late final Index idxAssetFacePersonId = Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + late final Index idxAssetFaceAssetId = Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + late final Index idxAssetFaceVisiblePerson = Index( + 'idx_asset_face_visible_person', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + late final Index idxAssetEditAssetId = Index( + 'idx_asset_edit_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetOwnerVisibilityDeletedCreated, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + assetEditEntity, + settings, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteExifCity, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxAssetFaceVisiblePerson, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + idxAssetEditAssetId, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('remote_asset_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('stack_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('remote_album_entity', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('local_album_entity', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'local_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('local_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'local_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('local_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('user_metadata_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('partner_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('partner_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('remote_exif_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_asset_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_album_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_user_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_album_user_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('remote_asset_cloud_id_entity', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('memory_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('memory_asset_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'memory_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('memory_asset_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('person_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('asset_face_entity', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'person_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('asset_face_entity', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('asset_edit_entity', kind: UpdateKind.delete)], + ), + ]); + @override + int get schemaVersion => 27; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 74ecf39038..9c1cdae416 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -2,7 +2,7 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; @@ -18,7 +18,7 @@ import 'package:mocktail/mocktail.dart'; class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} -class MockMetadataRepository extends Mock implements MetadataRepository {} +class MockSettingsRepository extends Mock implements SettingsRepository {} class MockLogRepository extends Mock implements LogRepository {} diff --git a/mobile/test/medium/repositories/metadata_repository_test.dart b/mobile/test/medium/repositories/settings_repository_test.dart similarity index 54% rename from mobile/test/medium/repositories/metadata_repository_test.dart rename to mobile/test/medium/repositories/settings_repository_test.dart index 7b185f3bec..6a3f79badb 100644 --- a/mobile/test/medium/repositories/metadata_repository_test.dart +++ b/mobile/test/medium/repositories/settings_repository_test.dart @@ -2,19 +2,19 @@ import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; -import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; +import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import '../repository_context.dart'; void main() { late MediumRepositoryContext ctx; - late MetadataRepository sut; + late SettingsRepository sut; setUpAll(() async { ctx = MediumRepositoryContext(); - sut = await MetadataRepository.ensureInitialized(ctx.db); + sut = await SettingsRepository.ensureInitialized(ctx.db); }); tearDownAll(() async { @@ -22,8 +22,8 @@ void main() { }); setUp(() async { - await ctx.db.delete(ctx.db.metadataEntity).go(); - await MetadataRepository.refresh(); + await ctx.db.delete(ctx.db.settingsEntity).go(); + await SettingsRepository.instance.refresh(); }); group('defaults', () { @@ -31,8 +31,8 @@ void main() { expect(sut.appConfig.theme.mode, ThemeMode.system); }); - test('systemConfig returns key defaults when DB is empty', () { - expect(sut.systemConfig.logLevel, LogLevel.info); + test('appConfig returns key defaults when DB is empty', () { + expect(sut.appConfig.logLevel, LogLevel.info); }); }); @@ -46,30 +46,30 @@ void main() { await sut.write(.themeMode, ThemeMode.light); await sut.write(.logLevel, LogLevel.severe); expect(sut.appConfig.theme.mode, ThemeMode.light); - expect(sut.systemConfig.logLevel, LogLevel.severe); + expect(sut.appConfig.logLevel, LogLevel.severe); }); - }); - group('delete', () { test('removes the row and reverts to default', () async { await sut.write(.themeMode, ThemeMode.dark); expect(sut.appConfig.theme.mode, ThemeMode.dark); - await sut.delete(.themeMode); + await sut.write(.themeMode, ThemeMode.system); expect(sut.appConfig.theme.mode, ThemeMode.system); - final rows = await ctx.db.select(ctx.db.metadataEntity).get(); + final rows = await ctx.db.select(ctx.db.settingsEntity).get(); expect(rows, isEmpty); }); }); - group('refresh', () { + group('delete', () {}); + + group('sync', () { test('picks up rows that were inserted directly into the DB', () async { await ctx.db - .into(ctx.db.metadataEntity) + .into(ctx.db.settingsEntity) .insert( - MetadataEntityCompanion.insert( - key: MetadataKey.themeMode.key, + SettingsEntityCompanion.insert( + key: SettingsKey.themeMode.name, value: ThemeMode.dark.name, updatedAt: Value(DateTime.now()), ), @@ -78,58 +78,46 @@ void main() { // Cache hasn't seen this row yet — view still returns the default. expect(sut.appConfig.theme.mode, ThemeMode.system); - await MetadataRepository.refresh(); + await SettingsRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.dark); }); test('drops cached values for rows that were deleted out from under the repo', () async { await sut.write(.themeMode, ThemeMode.dark); // Wipe the row directly. Cache still holds the old value. - await ctx.db.delete(ctx.db.metadataEntity).go(); + await ctx.db.delete(ctx.db.settingsEntity).go(); expect(sut.appConfig.theme.mode, ThemeMode.dark); - await MetadataRepository.refresh(); + await SettingsRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.system); }); - test('skips rows whose key is unknown to MetadataKey', () async { + test('skips rows whose key is unknown to SettingsKey', () async { await ctx.db - .into(ctx.db.metadataEntity) + .into(ctx.db.settingsEntity) .insert( - MetadataEntityCompanion.insert( + SettingsEntityCompanion.insert( key: 'app-config.unknown.future-key', value: 'whatever', updatedAt: Value(DateTime.now()), ), ); - await MetadataRepository.refresh(); + await SettingsRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.system); }); }); group('watch', () { test('watchAppConfig emits the new value after a write', () async { - final expectation = expectLater(sut.watchAppConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark)); - await sut.write(MetadataKey.themeMode, ThemeMode.dark); + final expectation = expectLater(sut.watchConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark)); + await sut.write(SettingsKey.themeMode, ThemeMode.dark); await expectation; }); - test('watchAppConfig does not emit when only system-config rows change', () async { - final emissions = []; - // skip(1) drops the on-subscribe replay so we only capture emissions caused by the write below. - final sub = sut.watchAppConfig().skip(1).listen((c) => emissions.add(c.theme.mode)); - - await sut.write(MetadataKey.logLevel, LogLevel.severe); - await pumpEventQueue(); - await sub.cancel(); - - expect(emissions, isEmpty); - }); - - test('watchSystemConfig emits the new value after a write', () async { - final expectation = expectLater(sut.watchSystemConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning)); - await sut.write(MetadataKey.logLevel, LogLevel.warning); + test('watchConfig emits the new value after a write', () async { + final expectation = expectLater(sut.watchConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning)); + await sut.write(SettingsKey.logLevel, LogLevel.warning); await expectation; }); }); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index dd19f2b1cc..310f2f4d49 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -38,7 +38,7 @@ void main() { ); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); await StoreService.init(storeRepository: DriftStoreRepository(db)); - await MetadataRepository.ensureInitialized(db); + await SettingsRepository.ensureInitialized(db); await Store.put(StoreKey.serverEndpoint, 'http://test-server.com'); await Store.put(StoreKey.deviceId, 'test-device-id'); diff --git a/mobile/test/unit/repositories/metadata_repository_test.dart b/mobile/test/unit/repositories/metadata_repository_test.dart deleted file mode 100644 index 75b34da7cb..0000000000 --- a/mobile/test/unit/repositories/metadata_repository_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; - -void main() { - group('MetadataKey', () { - test('every key round-trips its default value losslessly', () { - for (final key in MetadataKey.values) { - final encoded = key.encode(key.defaultValue); - final decoded = key.decode(encoded); - expect(decoded, key.defaultValue, reason: 'round-trip failed for ${key.name}'); - } - }); - - test('decode falls back to the default value when the raw input is unparseable', () { - for (final key in MetadataKey.values) { - // String keys can decode any string. So skip them - if (key.defaultValue is String) { - continue; - } - expect( - key.decode('not a valid encoding for any key'), - key.defaultValue, - reason: 'fallback failed for ${key.name}', - ); - } - }); - }); -} diff --git a/mobile/test/unit/repositories/settings_repository_test.dart b/mobile/test/unit/repositories/settings_repository_test.dart new file mode 100644 index 0000000000..80214dd298 --- /dev/null +++ b/mobile/test/unit/repositories/settings_repository_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; + +void main() { + group('SettingsKey', () { + for (final key in SettingsKey.values) { + test('verify codec for $key', () { + final defaultValue = defaultConfig.read(key); + final encoded = key.encode(defaultValue); + final decoded = key.decode(encoded); + expect(decoded, defaultValue, reason: 'round-trip failed for ${key.name}'); + }); + } + }); +} diff --git a/mobile/test/unit/utils/semver_test.dart b/mobile/test/unit/utils/semver_test.dart index 8f1958a879..1e534af593 100644 --- a/mobile/test/unit/utils/semver_test.dart +++ b/mobile/test/unit/utils/semver_test.dart @@ -88,5 +88,71 @@ void main() { expect(version2.minor, 2); expect(version2.patch, 3); }); + + test('Orders later prerelease above earlier prerelease', () { + const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1); + const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2); + expect(rc2 > rc1, isTrue); + expect(rc1 < rc2, isTrue); + expect(rc1 == rc2, isFalse); + }); + + test('Final release outranks its prerelease of the same version', () { + const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1); + const release = SemVer(major: 1, minor: 151, patch: 0); + expect(release > rc, isTrue); + expect(rc < release, isTrue); + }); + + test('Higher major outranks a prerelease regardless of ordinal', () { + const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 9); + const next = SemVer(major: 2, minor: 0, patch: 0); + expect(next > rc, isTrue); + }); + + test('Equal prerelease versions compare as equal', () { + const a = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3); + const b = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3); + expect(a == b, isTrue); + expect(a > b, isFalse); + expect(a < b, isFalse); + }); + + test('Reports prerelease difference type', () { + const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1); + const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2); + expect(rc1.differenceType(rc2), SemVerType.prerelease); + }); + + test('toString includes prerelease suffix when present', () { + const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2); + expect(rc.toString(), '1.151.0-rc.2'); + }); + + test('Parses prerelease ordinal from -rc strings', () { + final dotted = SemVer.fromString('1.151.0-rc.2'); + expect(dotted.major, 1); + expect(dotted.minor, 151); + expect(dotted.patch, 0); + expect(dotted.prerelease, 2); + + expect(SemVer.fromString('v1.151.0-rc.3').prerelease, 3); + expect(SemVer.fromString('1.2.3-rc.2+build.5').prerelease, 2); + }); + + test('Plain version string has null prerelease', () { + expect(SemVer.fromString('3.0.0').prerelease, isNull); + }); + + test('Invalid rc suffixes parse without error and have null prerelease', () { + final debug = SemVer.fromString('1.2.3-debug'); + expect(debug.major, 1); + expect(debug.minor, 2); + expect(debug.patch, 3); + expect(debug.prerelease, isNull); + + expect(SemVer.fromString('1.2.3+build.5').prerelease, isNull); + expect(SemVer.fromString('1.151.0-rc4').prerelease, isNull); + }); }); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index fcc5c6ff32..33eaf13fc2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1627,6 +1627,17 @@ "type": "string" } }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Album ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, { "name": "isOwned", "required": false, @@ -1644,6 +1655,15 @@ "schema": { "type": "boolean" } + }, + { + "name": "name", + "required": false, + "in": "query", + "description": "Album name (exact match)", + "schema": { + "type": "string" + } } ], "responses": { @@ -4288,6 +4308,351 @@ "x-immich-state": "Stable" } }, + "/assets/{id}/video/stream/main.m3u8": { + "get": { + "description": "Returns an HLS main playlist with all available variants for the asset.", + "operationId": "getMainPlaylist", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/vnd.apple.mpegurl": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get HLS main playlist", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, + "/assets/{id}/video/stream/{sessionId}": { + "delete": { + "description": "Releases server resources for the streaming session.", + "operationId": "endSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sessionId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "End HLS streaming session", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, + "/assets/{id}/video/stream/{sessionId}/{variantIndex}/playlist.m3u8": { + "get": { + "description": "Returns an HLS media playlist for one variant of the streaming session.", + "operationId": "getMediaPlaylist", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sessionId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "variantIndex", + "required": true, + "in": "path", + "schema": { + "minimum": 0, + "maximum": 9007199254740991, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/vnd.apple.mpegurl": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get HLS media playlist", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, + "/assets/{id}/video/stream/{sessionId}/{variantIndex}/{filename}": { + "get": { + "description": "Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s).", + "operationId": "getSegment", + "parameters": [ + { + "name": "filename", + "required": true, + "in": "path", + "schema": { + "pattern": "^(init\\.mp4|seg_\\d+\\.m4s)$", + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sessionId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "variantIndex", + "required": true, + "in": "path", + "schema": { + "minimum": 0, + "maximum": 9007199254740991, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get HLS segment or init file", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Alpha" + } + ], + "x-immich-permission": "asset.view", + "x-immich-state": "Alpha" + } + }, "/auth/admin-sign-up": { "post": { "description": "Create the first admin user in the system.", @@ -18126,6 +18491,7 @@ "LibrarySyncFilesQueueAll", "LibrarySyncFiles", "LibraryScanQueueAll", + "HlsSessionCleanup", "MemoryCleanup", "MemoryGenerate", "NotificationsCleanup", @@ -18151,7 +18517,7 @@ "VersionCheck", "OcrQueueAll", "Ocr", - "WorkflowAssetCreate" + "WorkflowAssetTrigger" ], "type": "string" }, @@ -20211,6 +20577,13 @@ "trigger": { "$ref": "#/components/schemas/WorkflowTrigger", "description": "Workflow trigger" + }, + "uiHints": { + "description": "Ui hints, for example \"smart-album\"", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -20218,7 +20591,8 @@ "key", "steps", "title", - "trigger" + "trigger", + "uiHints" ], "type": "object" }, @@ -20812,6 +21186,57 @@ ], "type": "string" }, + "ReleaseChannel": { + "description": "Release channel", + "enum": [ + "stable", + "releaseCandidate" + ], + "type": "string" + }, + "ReleaseEventV1": { + "properties": { + "checkedAt": { + "description": "When the server last checked for a latest version. As an ISO timestamp", + "type": "string" + }, + "isAvailable": { + "description": "Whether a new version is available", + "type": "boolean" + }, + "releaseVersion": { + "$ref": "#/components/schemas/ServerVersionResponseDto" + }, + "serverVersion": { + "$ref": "#/components/schemas/ServerVersionResponseDto" + }, + "type": { + "$ref": "#/components/schemas/ReleaseType", + "description": "Release type", + "nullable": true + } + }, + "required": [ + "checkedAt", + "isAvailable", + "releaseVersion", + "serverVersion", + "type" + ], + "type": "object" + }, + "ReleaseType": { + "enum": [ + "major", + "premajor", + "minor", + "preminor", + "patch", + "prepatch", + "prerelease" + ], + "type": "string" + }, "ReverseGeocodingStateResponseDto": { "properties": { "lastImportFileName": { @@ -21276,6 +21701,10 @@ "description": "Whether password login is enabled", "type": "boolean" }, + "realtimeTranscoding": { + "description": "Whether real-time transcoding is enabled", + "type": "boolean" + }, "reverseGeocoding": { "description": "Whether reverse geocoding is enabled", "type": "boolean" @@ -21308,6 +21737,7 @@ "oauthAutoLaunch", "ocr", "passwordLogin", + "realtimeTranscoding", "reverseGeocoding", "search", "sidecar", @@ -21488,26 +21918,40 @@ "major": { "description": "Major version number", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, "minor": { "description": "Minor version number", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, "patch": { "description": "Patch version number", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" + }, + "prerelease": { + "description": "Pre-release version number", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer", + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ] } }, "required": [ "major", "minor", - "patch" + "patch", + "prerelease" ], "type": "object" }, @@ -24168,6 +24612,9 @@ "description": "Preset", "type": "string" }, + "realtime": { + "$ref": "#/components/schemas/SystemConfigFFmpegRealtimeDto" + }, "refs": { "description": "References", "maximum": 6, @@ -24218,6 +24665,7 @@ "maxBitrate", "preferredHwDevice", "preset", + "realtime", "refs", "targetAudioCodec", "targetResolution", @@ -24230,6 +24678,18 @@ ], "type": "object" }, + "SystemConfigFFmpegRealtimeDto": { + "properties": { + "enabled": { + "description": "Enable real-time HLS transcoding (alpha)", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, "SystemConfigFacesDto": { "properties": { "import": { @@ -24528,12 +24988,16 @@ }, "SystemConfigNewVersionCheckDto": { "properties": { + "channel": { + "$ref": "#/components/schemas/ReleaseChannel" + }, "enabled": { "description": "Enabled", "type": "boolean" } }, "required": [ + "channel", "enabled" ], "type": "object" @@ -26346,6 +26810,7 @@ "description": "Plugin trigger type", "enum": [ "AssetCreate", + "AssetMetadataExtraction", "PersonRecognized" ], "type": "string" diff --git a/open-api/patch/api_client.dart.patch b/open-api/patch/api_client.dart.patch index 8996e79413..55acb0d3cd 100644 --- a/open-api/patch/api_client.dart.patch +++ b/open-api/patch/api_client.dart.patch @@ -1,21 +1,96 @@ -@@ -143,19 +143,19 @@ - ); +@@ -13,7 +13,7 @@ + class ApiClient { + ApiClient({this.basePath = '/api', this.authentication,}); + +- final String basePath; ++ String basePath; + final Authentication? authentication; + + var _client = Client(); +@@ -44,8 +44,9 @@ + Object? body, + Map headerParams, + Map formParams, +- String? contentType, +- ) async { ++ String? contentType, { ++ Future? abortTrigger, ++ }) async { + await authentication?.applyToParams(queryParams, headerParams); + + headerParams.addAll(_defaultHeaderMap); +@@ -63,7 +64,7 @@ + body is MultipartFile && (contentType == null || + !contentType.toLowerCase().startsWith('multipart/form-data')) + ) { +- final request = StreamedRequest(method, uri); ++ final request = AbortableStreamedRequest(method, uri, abortTrigger: abortTrigger); + request.headers.addAll(headerParams); + request.contentLength = body.length; + body.finalize().listen( +@@ -78,7 +79,7 @@ + } + + if (body is MultipartRequest) { +- final request = MultipartRequest(method, uri); ++ final request = AbortableMultipartRequest(method, uri, abortTrigger: abortTrigger); + request.fields.addAll(body.fields); + request.files.addAll(body.files); + request.headers.addAll(body.headers); +@@ -92,14 +93,19 @@ + : await serializeAsync(body); + final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; + +- switch(method) { +- case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); +- case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); +- case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); ++ final request = AbortableRequest(method, uri, abortTrigger: abortTrigger); ++ if (nullableHeaderParams != null) { ++ request.headers.addAll(nullableHeaderParams); + } ++ if (msgBody is String) { ++ request.body = msgBody; ++ } else if (msgBody is List) { ++ request.bodyBytes = msgBody; ++ } else if (msgBody is Map) { ++ request.bodyFields = msgBody; ++ } ++ final response = await _client.send(request); ++ return Response.fromStream(response); + } on SocketException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, +@@ -136,26 +146,21 @@ + trace, + ); + } +- +- throw ApiException( +- HttpStatus.badRequest, +- 'Invalid HTTP operation: $method $path', +- ); } - + - Future deserializeAsync(String value, String targetType, {bool growable = false,}) async => + Future deserializeAsync(String value, String targetType, {bool growable = false,}) => // ignore: deprecated_member_use_from_same_package deserialize(value, targetType, growable: growable); - + @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') - dynamic deserialize(String value, String targetType, {bool growable = false,}) { + Future deserialize(String value, String targetType, {bool growable = false,}) async { // Remove all spaces. Necessary for regular expressions as well. targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments - + // If the expected target type is String, nothing to do... return targetType == 'String' ? value - : fromJson(json.decode(value), targetType, growable: growable); + : fromJson(await compute((String j) => json.decode(j), value), targetType, growable: growable); } + + // ignore: deprecated_member_use_from_same_package diff --git a/open-api/templates/mobile/api.mustache b/open-api/templates/mobile/api.mustache index ac32571123..2cd4c0f04e 100644 --- a/open-api/templates/mobile/api.mustache +++ b/open-api/templates/mobile/api.mustache @@ -49,7 +49,7 @@ class {{{classname}}} { /// {{/-last}} {{/allParams}} - Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { + Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'{{{path}}}'{{#pathParams}} .replaceAll({{=<% %>=}}'{<% baseName %>}'<%={{ }}=%>, {{{paramName}}}{{^isString}}.toString(){{/isString}}){{/pathParams}}; @@ -128,6 +128,7 @@ class {{{classname}}} { headerParams, formParams, contentTypes.isEmpty ? null : contentTypes.first, + abortTrigger: abortTrigger, ); } @@ -161,8 +162,8 @@ class {{{classname}}} { /// {{/-last}} {{/allParams}} - Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { - final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}}); + Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { + final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}}, {{/required}}{{/allParams}}{{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}}, {{/required}}{{/allParams}}abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/templates/mobile/api.mustache.patch b/open-api/templates/mobile/api.mustache.patch index e3f888d6d7..feb5f40047 100644 --- a/open-api/templates/mobile/api.mustache.patch +++ b/open-api/templates/mobile/api.mustache.patch @@ -1,8 +1,11 @@ ---- api.mustache 2025-01-22 05:50:25 -+++ api.mustache.modified 2025-01-22 05:52:23 -@@ -51,7 +51,7 @@ +--- api.mustache ++++ api.mustache.modified +@@ -49,9 +49,9 @@ + /// + {{/-last}} {{/allParams}} - Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { +- Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { ++ Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { // ignore: prefer_const_declarations - final path = r'{{{path}}}'{{#pathParams}} + final apiPath = r'{{{path}}}'{{#pathParams}} @@ -18,7 +21,7 @@ {{#formParams}} {{^isFile}} if ({{{paramName}}} != null) { -@@ -121,7 +121,7 @@ +@@ -121,13 +121,14 @@ {{/isMultipart}} return apiClient.invokeAPI( @@ -27,3 +30,21 @@ '{{{httpMethod}}}', queryParams, postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, ++ abortTrigger: abortTrigger, + ); + } + +@@ -161,8 +162,8 @@ + /// + {{/-last}} + {{/allParams}} +- Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { +- final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}}); ++ Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { ++ final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}}, {{/required}}{{/allParams}}{{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}}, {{/required}}{{/allParams}}abortTrigger: abortTrigger,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json index 3111678862..48b4bee2c8 100644 --- a/packages/plugin-core/manifest.json +++ b/packages/plugin-core/manifest.json @@ -7,8 +7,8 @@ "wasmPath": "dist/plugin.wasm", "templates": [ { - "name": "auto-archive-screenshots", - "title": "Auto-archive screenshots", + "name": "screenshots-smart-album", + "title": "Archive screenshots", "description": "Archive uploads with \"screenshot\" in the filename and optionally add them to an album", "trigger": "AssetCreate", "steps": [ @@ -20,19 +20,41 @@ "caseSensitive": false } }, - { - "method": "immich-plugin-core#assetAddToAlbums", - "config": { - "albumIds": [] - } - }, { "method": "immich-plugin-core#assetArchive", "config": { "inverse": false } + }, + { + "method": "immich-plugin-core#assetAddToAlbums", + "config": { + "albumName": "Screenshots", + "albumIds": [] + } } - ] + ], + "uiHints": ["SmartAlbum"] + }, + { + "name": "missing-timezone-smart-album", + "title": "Missing timezone", + "description": "Automatically create an album for assets without a time zone", + "trigger": "AssetMetadataExtraction", + "steps": [ + { + "method": "immich-plugin-core#assetMissingTimeZoneFilter", + "config": {} + }, + { + "method": "immich-plugin-core#assetAddToAlbums", + "config": { + "albumName": "Missing time zone", + "albumIds": [] + } + } + ], + "uiHints": ["SmartAlbum"] } ], "methods": [ @@ -65,7 +87,25 @@ }, "required": ["pattern"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] + }, + { + "name": "assetMissingTimeZoneFilter", + "title": "Filter by missing time zone", + "description": "Filter assets that have no time zone information", + "types": ["AssetV1"], + "schema": { + "type": "object", + "properties": { + "inverse": { + "type": "boolean", + "title": "Inverse", + "description": "Missing by default, set to true to filter assets with a time zone", + "default": false + } + } + }, + "uiHints": ["Filter"] }, { "name": "filterFileType", @@ -85,7 +125,7 @@ }, "required": ["fileTypes"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "filterPerson", @@ -99,7 +139,7 @@ "array": true, "title": "Person IDs", "description": "List of person to match", - "uiHint": "personI" + "uiHint": "personId" }, "matchAny": { "type": "boolean", @@ -110,7 +150,7 @@ }, "required": ["personIds"] }, - "uiHints": ["filter"] + "uiHints": ["Filter"] }, { "name": "assetArchive", @@ -187,7 +227,12 @@ "title": "Album IDs", "array": true, "description": "Target album IDs", - "uiHint": "albumId" + "uiHint": "AlbumId" + }, + "albumName": { + "type": "string", + "title": "Album name", + "description": "Use an album with this name if one exists, otherwise create a new one" } }, "required": ["albumIds"] @@ -272,14 +317,14 @@ "type": "string", "title": "Album ID", "description": "Target album ID", - "uiHint": "albumId" + "uiHint": "AlbumId" }, "albumIds": { "type": "string", "title": "Album IDs", "description": "Target album IDs", "array": true, - "uiHint": "albumId" + "uiHint": "AlbumId" } } } diff --git a/packages/plugin-core/package.json b/packages/plugin-core/package.json index 7c0bdf9af2..26b5124426 100644 --- a/packages/plugin-core/package.json +++ b/packages/plugin-core/package.json @@ -13,6 +13,7 @@ "license": "AGPL-3.0", "devDependencies": { "@extism/js-pdk": "^1.0.1", + "@immich/sdk": "workspace:*", "@immich/plugin-sdk": "workspace:*", "esbuild": "^0.28.0", "typescript": "^6.0.0" diff --git a/packages/plugin-core/src/index.d.ts b/packages/plugin-core/src/index.d.ts index ae45184cbe..170fa13102 100644 --- a/packages/plugin-core/src/index.d.ts +++ b/packages/plugin-core/src/index.d.ts @@ -1,14 +1,20 @@ -// copy from -// import '@immich/plugin-sdk/host-functions'; +// keep in sync with plugin-sdk/host-functions.ts'; declare module 'extism:host' { interface user { - albumAddAssets(ptr: PTR): I64; + searchAlbums(ptr: PTR): I64; + createAlbum(ptr: PTR): I64; + addAssetsToAlbum(ptr: PTR): I64; addAssetsToAlbums(ptr: PTR): I64; } } +// keep in sync with manifest.json declare module 'main' { + // filters export function assetFileFilter(): I32; + export function assetMissingTimeZoneFilter(): I32; + + // updates export function assetFavorite(): I32; export function assetVisibility(): I32; export function assetArchive(): I32; diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index 85a4a449e7..bcb05cfa19 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -1,4 +1,5 @@ -import { AssetStatus, AssetVisibility, WorkflowType, wrapper } from '@immich/plugin-sdk'; +import { wrapper } from '@immich/plugin-sdk'; +import { AssetVisibility, WorkflowType } from '@immich/sdk'; type AssetFileFilterConfig = { pattern: string; @@ -41,6 +42,14 @@ export const assetFileFilter = () => { }); }; +export const assetMissingTimeZoneFilter = () => { + return wrapper(({ config, data }) => { + const hasTimeZone = !!data.asset?.exifInfo?.timeZone; + const needsTimeZone = config.inverse ? true : false; + return { workflow: { continue: hasTimeZone === needsTimeZone } }; + }); +}; + export const assetFavorite = () => { return wrapper(({ config, data }) => { const target = config.inverse ? false : true; @@ -89,28 +98,35 @@ export const assetLock = () => { }; export const assetTrash = () => { - return wrapper(({ config, data }) => ({ - changes: { - asset: config.inverse - ? { deletedAt: null, status: AssetStatus.Active } - : { deletedAt: new Date(), status: AssetStatus.Trashed }, - }, - })); + // TODO use trash/untrash host functions + return wrapper(() => ({})); }; export const assetAddToAlbums = () => { - return wrapper(({ config, data, functions }) => { + return wrapper(({ config, data, functions }) => { + const assetId = data.asset.id; + if (config.albumIds.length === 0) { - // noop - return {}; + if (!config.albumName) { + return {}; + } + + const [existing] = functions.searchAlbums({ name: config.albumName }); + if (!existing) { + const created = functions.createAlbum({ albumName: config.albumName, assetIds: [assetId] }); + config.albumIds.push(created.id); + return {}; + } + + config.albumIds.push(existing.id); } if (config.albumIds.length === 1) { - functions.albumAddAssets(config.albumIds[0], [data.asset.id]); + functions.addAssetsToAlbum(config.albumIds[0], [assetId]); return {}; } - functions.addAssetsToAlbums({ albumIds: config.albumIds, assetIds: [data.asset.id] }); + functions.addAssetsToAlbums({ albumIds: config.albumIds, assetIds: [assetId] }); return {}; }); }; diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index f505a6cc0e..7c44368fbc 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -2,7 +2,6 @@ "name": "@immich/plugin-sdk", "version": "0.0.0", "description": "", - "main": "index.js", "type": "module", "exports": { "./host-functions": { @@ -11,7 +10,8 @@ }, ".": { "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "default": "./dist/index.js" } }, "scripts": { @@ -27,6 +27,7 @@ "packageManager": "pnpm@10.33.4", "devDependencies": { "@extism/js-pdk": "^1.1.1", + "@immich/sdk": "workspace:*", "@types/node": "^24.12.4", "esbuild": "^0.28.0", "tsc-alias": "^1.8.16", diff --git a/packages/plugin-sdk/src/enum.ts b/packages/plugin-sdk/src/enum.ts deleted file mode 100644 index a11dab64da..0000000000 --- a/packages/plugin-sdk/src/enum.ts +++ /dev/null @@ -1,33 +0,0 @@ -export enum WorkflowTrigger { - AssetCreate = 'AssetCreate', - PersonRecognized = 'PersonRecognized', -} - -export enum WorkflowType { - AssetV1 = 'AssetV1', - AssetPersonV1 = 'AssetPersonV1', -} - -export enum AssetType { - Image = 'IMAGE', - Video = 'VIDEO', - Audio = 'AUDIO', - Other = 'OTHER', -} - -export enum AssetStatus { - Active = 'active', - Trashed = 'trashed', - Deleted = 'deleted', -} - -export enum AssetVisibility { - Archive = 'archive', - Timeline = 'timeline', - - /** - * Video part of the LivePhotos and MotionPhotos - */ - Hidden = 'hidden', - Locked = 'locked', -} diff --git a/packages/plugin-sdk/src/host-functions.ts b/packages/plugin-sdk/src/host-functions.ts index d0f8a3ef17..281e27c83c 100644 --- a/packages/plugin-sdk/src/host-functions.ts +++ b/packages/plugin-sdk/src/host-functions.ts @@ -1,12 +1,26 @@ +import { + getAllAlbums, + type AlbumResponseDto, + type BulkIdResponseDto, + type BulkIdsDto, + type CreateAlbumDto, +} from '@immich/sdk'; + +// keep in sync with plugin-core/src/index.d.ts'; declare module 'extism:host' { interface user { - albumAddAssets(ptr: PTR): I64; + searchAlbums(ptr: PTR): I64; + createAlbum(ptr: PTR): I64; + addAssetsToAlbum(ptr: PTR): I64; addAssetsToAlbums(ptr: PTR): I64; } } -const host = Host.getFunctions(); -type HostFunctionName = keyof typeof host; +type AlbumsToAssets = { + assetIds: string[]; + albumIds: string[]; +}; + type HostFunctionSuccessResult = { success: true; response: T }; type HostFunctionErrorResult = { success: false; @@ -17,35 +31,49 @@ type HostFunctionResult = | HostFunctionSuccessResult | HostFunctionErrorResult; -const call = (name: HostFunctionName, authToken: string, args: T) => { - const pointer1 = Memory.fromString(JSON.stringify({ authToken, args })); - const fn = host[name]; - const handler = Memory.find(fn(pointer1.offset)); +type QueryParams any> = Parameters[0]; +type AlbumSearchDto = QueryParams; - try { - const result = JSON.parse(handler.readString()) as HostFunctionResult; +export const hostFunctions = (authToken: string) => { + const host = Host.getFunctions(); + type HostFunctionName = keyof typeof host; - if (result.success) { - return result.response; + const call = (name: HostFunctionName, authToken: string, args: T) => { + const pointer1 = Memory.fromString(JSON.stringify({ authToken, args })); + const fn = host[name]; + const handler = Memory.find(fn(pointer1.offset)); + + try { + const result = JSON.parse(handler.readString()) as HostFunctionResult; + + if (result.success) { + return result.response; + } + + throw new Error( + `Failed to call host function "${String(name)}", received ${result.status} - ${JSON.stringify(result.message)}`, + ); + } finally { + handler.free(); + pointer1.free(); } + }; - throw new Error( - `Failed to call host function "${String(name)}", received ${result.status} - ${JSON.stringify(result.message)}`, - ); - } finally { - handler.free(); - pointer1.free(); - } + return { + // album + searchAlbums: (dto: AlbumSearchDto) => + call<[AlbumSearchDto], AlbumResponseDto[]>('searchAlbums', authToken, [ + dto, + ]), + createAlbum: (dto: CreateAlbumDto) => + call<[CreateAlbumDto], AlbumResponseDto>('createAlbum', authToken, [dto]), + addAssetsToAlbum: (albumId: string, assetIds: string[]) => + call<[string, BulkIdsDto], BulkIdResponseDto[]>( + 'addAssetsToAlbum', + authToken, + [albumId, { ids: assetIds }], + ), + addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) => + call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]), + }; }; - -type AlbumsToAssets = { - assetIds: string[]; - albumIds: string[]; -}; - -export const hostFunctions = (authToken: string) => ({ - albumAddAssets: (albumId: string, assetIds: string[]) => - call('albumAddAssets', authToken, [albumId, { ids: assetIds }]), - addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) => - call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]), -}); diff --git a/packages/plugin-sdk/src/index.ts b/packages/plugin-sdk/src/index.ts index 6d4deb2053..c83369410a 100644 --- a/packages/plugin-sdk/src/index.ts +++ b/packages/plugin-sdk/src/index.ts @@ -1,4 +1,3 @@ -export * from 'src/enum.js'; export * from 'src/host-functions.js'; export * from 'src/sdk.js'; export * from 'src/types.js'; diff --git a/packages/plugin-sdk/src/sdk.ts b/packages/plugin-sdk/src/sdk.ts index f0ff8723a6..283e9c3dd5 100644 --- a/packages/plugin-sdk/src/sdk.ts +++ b/packages/plugin-sdk/src/sdk.ts @@ -1,9 +1,10 @@ -import type { WorkflowType } from 'src/enum.js'; +import type { WorkflowType } from '@immich/sdk'; import { hostFunctions } from 'src/host-functions.js'; import type { ConfigValue, WorkflowEventPayload, WorkflowResponse, + WorkflowStepConfig, } from 'src/types.js'; export const wrapper = < @@ -19,19 +20,28 @@ export const wrapper = < const input = Host.inputString(); try { - const event = JSON.parse(input) as WorkflowEventPayload; - // const debug = event.workflow.debug ?? false; + const payload = JSON.parse(input) as WorkflowEventPayload; + const event = { + ...payload, + functions: hostFunctions(payload.workflow.authToken), + }; + + const eventConfigBefore = JSON.stringify(event.config); console.debug( - `Inputs: trigger=${event.trigger}, event=${event.type}, config=${JSON.stringify(event.config)}`, + `Inputs: trigger=${event.trigger}, event=${event.type}, config=${eventConfigBefore}`, ); - const response = - fn({ ...event, functions: hostFunctions(event.workflow.authToken) }) ?? - {}; + const response = fn(event) ?? {}; + + // if config changed, notify host + const eventConfigAfter = JSON.stringify(event.config); + if (!response.config && eventConfigBefore !== eventConfigAfter) { + response.config = event.config as WorkflowStepConfig; + } console.debug( - `Outputs: workflow=${JSON.stringify(response.workflow)}, changes=${JSON.stringify(response.changes)}, data=${JSON.stringify(response.data)}`, + `Outputs: workflow=${JSON.stringify(response.workflow)}, changes=${JSON.stringify(response.changes)}, data=${JSON.stringify(response.data)}, config=${JSON.stringify(response.config)}`, ); const output = JSON.stringify(response); diff --git a/packages/plugin-sdk/src/types.ts b/packages/plugin-sdk/src/types.ts index 54cca3a5aa..67c179f4a6 100644 --- a/packages/plugin-sdk/src/types.ts +++ b/packages/plugin-sdk/src/types.ts @@ -1,10 +1,4 @@ -import type { - AssetStatus, - AssetType, - AssetVisibility, - WorkflowTrigger, - WorkflowType, -} from 'src/enum.js'; +import type { AssetTypeEnum, AssetVisibility, WorkflowType } from '@immich/sdk'; type DeepPartial = T extends Date ? T @@ -21,6 +15,12 @@ export type WorkflowEventMap = { export type WorkflowEventData = WorkflowEventMap[T]; +export enum WorkflowTrigger { + AssetCreate = 'AssetCreate', + AssetMetadataExtraction = 'AssetMetadataExtraction', + PersonRecognized = 'PersonRecognized', +} + export type WorkflowEventPayload< T extends WorkflowType = WorkflowType, TConfig = WorkflowStepConfig, @@ -48,6 +48,8 @@ export type WorkflowResponse = { changes?: WorkflowChanges; /** data to be passed to the next workflow step */ data?: Record; + /** update step config */ + config?: WorkflowStepConfig; }; export type WorkflowStepConfig = { @@ -66,24 +68,23 @@ export type AssetV1 = { asset: { id: string; ownerId: string; - type: AssetType; + type: AssetTypeEnum; originalPath: string; - fileCreatedAt: Date; - fileModifiedAt: Date; + fileCreatedAt: string; + fileModifiedAt: string; isFavorite: boolean; checksum: Buffer; // sha1 checksum livePhotoVideoId: string | null; - updatedAt: Date; - createdAt: Date; + updatedAt: string; + createdAt: string; originalFileName: string; isOffline: boolean; libraryId: string | null; isExternal: boolean; - deletedAt: Date | null; - localDateTime: Date; + deletedAt: string | null; + localDateTime: string; stackId: string | null; duplicateId: string | null; - status: AssetStatus; visibility: AssetVisibility; isEdited: boolean; exifInfo: { @@ -93,8 +94,8 @@ export type AssetV1 = { exifImageHeight: number | null; fileSizeInByte: number | null; orientation: string | null; - dateTimeOriginal: Date | null; - modifyDate: Date | null; + dateTimeOriginal: string | null; + modifyDate: string | null; lensModel: string | null; fNumber: number | null; focalLength: number | null; @@ -116,7 +117,7 @@ export type AssetV1 = { autoStackId: string | null; rating: number | null; tags: string[] | null; - updatedAt: Date | null; + updatedAt: string | null; } | null; }; }; diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index e9cc8d4cb8..89d0e513d8 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -1539,6 +1539,8 @@ export type PluginTemplateResponseDto = { title: string; /** Workflow trigger */ trigger: WorkflowTrigger; + /** Ui hints, for example "smart-album" */ + uiHints: string[]; }; export type QueueResponseDto = { /** Whether the queue is paused */ @@ -1997,6 +1999,8 @@ export type ServerFeaturesDto = { ocr: boolean; /** Whether password login is enabled */ passwordLogin: boolean; + /** Whether real-time transcoding is enabled */ + realtimeTranscoding: boolean; /** Whether reverse geocoding is enabled */ reverseGeocoding: boolean; /** Whether search is enabled */ @@ -2080,6 +2084,8 @@ export type ServerVersionResponseDto = { minor: number; /** Patch version number */ patch: number; + /** Pre-release version number */ + prerelease: number | null; }; export type VersionCheckStateResponseDto = { /** Last check timestamp */ @@ -2255,6 +2261,10 @@ export type DatabaseBackupConfig = { export type SystemConfigBackupsDto = { database: DatabaseBackupConfig; }; +export type SystemConfigFFmpegRealtimeDto = { + /** Enable real-time HLS transcoding (alpha) */ + enabled: boolean; +}; export type SystemConfigFFmpegDto = { accel: TranscodeHWAccel; /** Accelerated decode */ @@ -2278,6 +2288,7 @@ export type SystemConfigFFmpegDto = { preferredHwDevice: string; /** Preset */ preset: string; + realtime: SystemConfigFFmpegRealtimeDto; /** References */ refs: number; targetAudioCodec: AudioCodec; @@ -2427,6 +2438,7 @@ export type SystemConfigMetadataDto = { faces: SystemConfigFacesDto; }; export type SystemConfigNewVersionCheckDto = { + channel: ReleaseChannel; /** Enabled */ enabled: boolean; }; @@ -2772,6 +2784,16 @@ export type WorkflowShareResponseDto = { trigger: WorkflowTrigger; }; export type LicenseResponseDto = UserLicense; +export type ReleaseEventV1 = { + /** When the server last checked for a latest version. As an ISO timestamp */ + checkedAt: string; + /** Whether a new version is available */ + isAvailable: boolean; + releaseVersion: ServerVersionResponseDto; + serverVersion: ServerVersionResponseDto; + /** Release type */ + "type": ReleaseType; +}; export type SyncAckV1 = {}; export type SyncAlbumDeleteV1 = { /** Album ID */ @@ -3603,18 +3625,22 @@ export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility } /** * List all albums */ -export function getAllAlbums({ assetId, isOwned, isShared }: { +export function getAllAlbums({ assetId, id, isOwned, isShared, name }: { assetId?: string; + id?: string; isOwned?: boolean; isShared?: boolean; + name?: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumResponseDto[]; }>(`/albums${QS.query(QS.explode({ assetId, + id, isOwned, - isShared + isShared, + name }))}`, { ...opts })); @@ -4212,6 +4238,82 @@ export function playAssetVideo({ id, key, slug }: { ...opts })); } +/** + * Get HLS main playlist + */ +export function getMainPlaylist({ id, key, slug }: { + id: string; + key?: string; + slug?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: string; + }>(`/assets/${encodeURIComponent(id)}/video/stream/main.m3u8${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} +/** + * End HLS streaming session + */ +export function endSession({ id, key, sessionId, slug }: { + id: string; + key?: string; + sessionId: string; + slug?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/assets/${encodeURIComponent(id)}/video/stream/${encodeURIComponent(sessionId)}${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts, + method: "DELETE" + })); +} +/** + * Get HLS media playlist + */ +export function getMediaPlaylist({ id, key, sessionId, slug, variantIndex }: { + id: string; + key?: string; + sessionId: string; + slug?: string; + variantIndex: number; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: string; + }>(`/assets/${encodeURIComponent(id)}/video/stream/${encodeURIComponent(sessionId)}/${encodeURIComponent(variantIndex)}/playlist.m3u8${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} +/** + * Get HLS segment or init file + */ +export function getSegment({ filename, id, key, sessionId, slug, variantIndex }: { + filename: string; + id: string; + key?: string; + sessionId: string; + slug?: string; + variantIndex: number; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/assets/${encodeURIComponent(id)}/video/stream/${encodeURIComponent(sessionId)}/${encodeURIComponent(variantIndex)}/${encodeURIComponent(filename)}${QS.query(QS.explode({ + key, + slug + }))}`, { + ...opts + })); +} /** * Register admin */ @@ -7081,6 +7183,7 @@ export enum WorkflowType { } export enum WorkflowTrigger { AssetCreate = "AssetCreate", + AssetMetadataExtraction = "AssetMetadataExtraction", PersonRecognized = "PersonRecognized" } export enum QueueJobStatus { @@ -7121,6 +7224,7 @@ export enum JobName { LibrarySyncFilesQueueAll = "LibrarySyncFilesQueueAll", LibrarySyncFiles = "LibrarySyncFiles", LibraryScanQueueAll = "LibraryScanQueueAll", + HlsSessionCleanup = "HlsSessionCleanup", MemoryCleanup = "MemoryCleanup", MemoryGenerate = "MemoryGenerate", NotificationsCleanup = "NotificationsCleanup", @@ -7146,7 +7250,7 @@ export enum JobName { VersionCheck = "VersionCheck", OcrQueueAll = "OcrQueueAll", Ocr = "Ocr", - WorkflowAssetCreate = "WorkflowAssetCreate" + WorkflowAssetTrigger = "WorkflowAssetTrigger" } export enum SearchSuggestionType { Country = "country", @@ -7311,6 +7415,10 @@ export enum LogLevel { Error = "error", Fatal = "fatal" } +export enum ReleaseChannel { + Stable = "stable", + ReleaseCandidate = "releaseCandidate" +} export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = "client_secret_post", ClientSecretBasic = "client_secret_basic" @@ -7319,6 +7427,15 @@ export enum AssetOrderBy { TakenAt = "takenAt", CreatedAt = "createdAt" } +export enum ReleaseType { + Major = "major", + Premajor = "premajor", + Minor = "minor", + Preminor = "preminor", + Patch = "patch", + Prepatch = "prepatch", + Prerelease = "prerelease" +} export enum UserMetadataKey { Preferences = "preferences", License = "license", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64cf0d3d85..debdb79b93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ overrides: sharp: ^0.34.5 webpackbar: ^7.0.0 -packageExtensionsChecksum: sha256-3l4AQg4iuprBDup+q+2JaPvbPg/7XodWCE0ZteH+s54= +packageExtensionsChecksum: sha256-W6pFzyf+6QXnV91iA6oob0OGVkergPXDN1afLgoF53k= pnpmfileChecksum: sha256-un98do36L0wZyqsjcLozQ3YUadCAn2yz5bXcBbOuyDA= @@ -320,6 +320,9 @@ importers: '@immich/plugin-sdk': specifier: workspace:* version: link:../plugin-sdk + '@immich/sdk': + specifier: workspace:* + version: link:../sdk esbuild: specifier: ^0.28.0 version: 0.28.0 @@ -332,6 +335,9 @@ importers: '@extism/js-pdk': specifier: ^1.1.1 version: 1.1.1 + '@immich/sdk': + specifier: workspace:* + version: link:../sdk '@types/node': specifier: ^24.12.4 version: 24.12.4 @@ -389,7 +395,7 @@ importers: version: 6.1.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) '@nestjs/swagger': specifier: ^11.4.2 - version: 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2) + version: 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3) '@nestjs/websockets': specifier: ^11.0.4 version: 11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-socket.io@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -536,7 +542,7 @@ importers: version: 8.0.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) nestjs-zod: specifier: ^5.3.0 - version: 5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + version: 5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^8.0.0 version: 8.0.7 @@ -571,8 +577,8 @@ importers: specifier: ^1.6.3 version: 1.6.4 semver: - specifier: ^7.6.2 - version: 7.8.0 + specifier: ^7.8.1 + version: 7.8.1 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -814,6 +820,12 @@ importers: happy-dom: specifier: ^20.0.0 version: 20.9.0 + hls-video-element: + specifier: ^1.5.11 + version: 1.5.11 + hls.js: + specifier: ^1.6.16 + version: 1.6.16 intl-messageformat: specifier: ^11.0.0 version: 11.2.6 @@ -3795,6 +3807,7 @@ packages: class-transformer: '*' class-validator: '*' reflect-metadata: ^0.1.12 || ^0.2.0 + typescript: '*' peerDependenciesMeta: '@fastify/static': optional: true @@ -6940,6 +6953,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + custom-media-element@1.4.6: + resolution: {integrity: sha512-/HRYqJOa1ob5ik4q7FIJVYxTJCFs/FL3+cQPAJjUf2uiqrDEzbTgB315gQ2rG8oK3w094W9m5tcB8S5Qah+caA==} + cytoscape-cose-bilkent@4.1.0: resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} peerDependencies: @@ -8264,6 +8280,12 @@ packages: history@4.10.1: resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + hls-video-element@1.5.11: + resolution: {integrity: sha512-tJJ65/52CDxj8XFyIve6zT9nVVdUIc6mqvKR25X0ycPKHk07rpjp4xxVteeCefDUBSf/tFLhlICFmn3KWj37xA==} + + hls.js@1.6.16: + resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + hogan.js@3.0.2: resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} hasBin: true @@ -9268,6 +9290,9 @@ packages: media-chrome@4.19.0: resolution: {integrity: sha512-HWhDTwts+BSbdPkkB1VsJXp5kvL0IxY7xFT5tBwliM2+89kTPVTnHnev+9it2f9PweANjT/C8/C/S0PW9oyZbA==} + media-tracks@0.3.5: + resolution: {integrity: sha512-l54rkKXlLBt3ob3zOLWHcnjvwUmX5bNEZ70igyapOZZC9imzqBmq1oz8p2roiV04KhjblFIi2hetLPF1oYVLRA==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -11243,6 +11268,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} @@ -16300,7 +16330,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.8.0 + semver: 7.8.1 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -16570,7 +16600,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3)': dependencies: '@microsoft/tsdoc': 0.16.0 '@nestjs/common': 11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16581,6 +16611,7 @@ snapshots: path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 swagger-ui-dist: 5.32.6 + typescript: 6.0.3 '@nestjs/testing@11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-express@11.1.21)': dependencies: @@ -17757,7 +17788,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -18461,7 +18492,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.59.4 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.1 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 @@ -19561,7 +19592,7 @@ snapshots: dot-prop: 10.1.0 env-paths: 3.0.0 json-schema-typed: 8.0.2 - semver: 7.8.0 + semver: 7.8.1 uint8array-extras: 1.5.0 config-chain@1.1.13: @@ -19733,7 +19764,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.15) postcss-modules-values: 4.0.0(postcss@8.5.15) postcss-value-parser: 4.2.0 - semver: 7.8.0 + semver: 7.8.1 optionalDependencies: webpack: 5.107.0(postcss@8.5.15) @@ -19856,6 +19887,8 @@ snapshots: csstype@3.2.3: {} + custom-media-element@1.4.6: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): dependencies: cose-base: 1.0.3 @@ -20601,7 +20634,7 @@ snapshots: find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 - semver: 7.8.0 + semver: 7.8.1 eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3): dependencies: @@ -20624,7 +20657,7 @@ snapshots: postcss: 8.5.15 postcss-load-config: 3.1.4(postcss@8.5.15) postcss-safe-parser: 7.0.1(postcss@8.5.15) - semver: 7.8.0 + semver: 7.8.1 svelte-eslint-parser: 1.6.1(svelte@5.55.8(@typescript-eslint/types@8.59.4)) optionalDependencies: svelte: 5.55.8(@typescript-eslint/types@8.59.4) @@ -21102,7 +21135,7 @@ snapshots: minimatch: 3.1.5 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.8.0 + semver: 7.8.1 tapable: 2.3.3 typescript: 5.9.3 webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0) @@ -21538,13 +21571,21 @@ snapshots: history@4.10.1: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 loose-envify: 1.4.0 resolve-pathname: 3.0.0 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 value-equal: 1.0.1 + hls-video-element@1.5.11: + dependencies: + custom-media-element: 1.4.6 + hls.js: 1.6.16 + media-tracks: 0.3.5 + + hls.js@1.6.16: {} + hogan.js@3.0.2: dependencies: mkdirp: 0.3.0 @@ -22126,7 +22167,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.0 + semver: 7.8.1 just-compare@2.3.0: {} @@ -22412,7 +22453,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 maplibre-gl@5.24.0: dependencies: @@ -22648,6 +22689,8 @@ snapshots: transitivePeerDependencies: - react + media-tracks@0.3.5: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -23229,14 +23272,14 @@ snapshots: '@opentelemetry/host-metrics': 0.38.3(@opentelemetry/api@1.9.1) tslib: 2.8.1 - nestjs-zod@5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + nestjs-zod@5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3))(rxjs@7.8.2)(zod@4.3.6): dependencies: '@nestjs/common': 11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2) deepmerge: 4.3.1 rxjs: 7.8.2 zod: 4.3.6 optionalDependencies: - '@nestjs/swagger': 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2) + '@nestjs/swagger': 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3) next-tick@1.1.0: {} @@ -23247,7 +23290,7 @@ snapshots: node-abi@3.92.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 optional: true node-abort-controller@3.1.1: {} @@ -23288,7 +23331,7 @@ snapshots: graceful-fs: 4.2.11 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.1 tar: 7.5.15 tinyglobby: 0.2.16 undici: 6.25.0 @@ -23526,7 +23569,7 @@ snapshots: got: 12.6.1 registry-auth-token: 5.1.1 registry-url: 6.0.1 - semver: 7.8.0 + semver: 7.8.1 package-manager-detector@1.6.0: {} @@ -23914,7 +23957,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@6.0.3) jiti: 1.21.7 postcss: 8.5.15 - semver: 7.8.0 + semver: 7.8.1 webpack: 5.107.0(postcss@8.5.15) transitivePeerDependencies: - typescript @@ -24969,12 +25012,14 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 semver@6.3.1: {} semver@7.8.0: {} + semver@7.8.1: {} + send@0.19.2: dependencies: debug: 2.6.9 @@ -25509,7 +25554,7 @@ snapshots: postcss: 8.5.15 postcss-scss: 4.0.9(postcss@8.5.15) postcss-selector-parser: 7.1.1 - semver: 7.8.0 + semver: 7.8.1 optionalDependencies: svelte: 5.55.8(@typescript-eslint/types@8.59.4) @@ -26217,7 +26262,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.8.0 + semver: 7.8.1 semver-diff: 4.0.0 xdg-basedir: 5.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f53cb0d406..a848deca82 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -60,6 +60,9 @@ packageExtensions: dependencies: node-addon-api: '*' node-gyp: '*' + '@nestjs/swagger': + peerDependencies: + typescript: '*' dedupePeerDependents: false preferWorkspacePackages: true injectWorkspacePackages: true diff --git a/server/Dockerfile b/server/Dockerfile index 6078a97504..60363c77b0 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -13,14 +13,15 @@ FROM builder AS server WORKDIR /usr/src/app COPY ./server ./server/ +COPY ./packages/sdk ./packages/sdk/ COPY ./packages/plugin-sdk ./packages/plugin-sdk/ RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \ --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ - SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --filter @immich/plugin-sdk --frozen-lockfile build && \ - SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned + SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter immich --frozen-lockfile build && \ + SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned FROM builder AS web @@ -55,7 +56,7 @@ FROM builder AS plugins ARG TARGETPLATFORM -COPY --from=ghcr.io/jdx/mise:2026.5.11@sha256:2ba959e4827f845fe0c4cfb4814089e790dc513040ef74f9e14925f446412a51 /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise WORKDIR /app COPY ./mise.toml ./mise.toml @@ -66,6 +67,7 @@ ENV MISE_DISABLE_TOOLS=flutter RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ mise install +COPY ./packages/sdk ./packages/sdk/ COPY ./packages/plugin-core ./packages/plugin-core/ COPY ./packages/plugin-sdk ./packages/plugin-sdk/ diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 7979e88dd3..e98f946c1e 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -2,7 +2,7 @@ FROM ghcr.io/immich-app/base-server-dev:202605051129@sha256:d07d8fcdb7e9f3ac22a811e87761ebf341ed0bb91956b89097540c2ed3fb9ca3 AS dev -COPY --from=ghcr.io/jdx/mise:2026.5.11@sha256:2ba959e4827f845fe0c4cfb4814089e790dc513040ef74f9e14925f446412a51 /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.5.18@sha256:5bb3311994fa78cef307ca3077cdb18f9551da0886371fc26ea91ab56220ffc5 /usr/local/bin/mise /usr/local/bin/mise RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \ echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \ diff --git a/server/mise.toml b/server/mise.toml index f7e4f92a26..1462af0c52 100644 --- a/server/mise.toml +++ b/server/mise.toml @@ -68,6 +68,7 @@ run = [ [tasks.ci-medium] run = [ { task = ":install" }, + { task = "//:plugins" }, { task = "//packages/plugin-core:build" }, { task = ":test-medium --run" }, ] diff --git a/server/package.json b/server/package.json index 119a1ea603..957aa548d3 100644 --- a/server/package.json +++ b/server/package.json @@ -106,7 +106,7 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", - "semver": "^7.6.2", + "semver": "^7.8.1", "sharp": "^0.34.5", "sirv": "^3.0.0", "socket.io": "^4.8.1", diff --git a/server/src/config.ts b/server/src/config.ts index 999e1e45bc..730663d046 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,4 +1,5 @@ import { CronExpression } from '@nestjs/schedule'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { AudioCodec, Colorspace, @@ -45,6 +46,9 @@ export type SystemConfig = { accel: TranscodeHardwareAcceleration; accelDecode: boolean; tonemap: ToneMapping; + realtime: { + enabled: boolean; + }; }; job: Record; logging: { @@ -135,6 +139,7 @@ export type SystemConfig = { }; newVersionCheck: { enabled: boolean; + channel: ReleaseChannel; }; nightlyTasks: { startTime: string; @@ -224,6 +229,9 @@ export const defaults = Object.freeze({ tonemap: ToneMapping.Hable, accel: TranscodeHardwareAcceleration.Disabled, accelDecode: true, + realtime: { + enabled: false, + }, }, job: { [QueueName.BackgroundTask]: { concurrency: 5 }, @@ -344,6 +352,7 @@ export const defaults = Object.freeze({ }, newVersionCheck: { enabled: true, + channel: ReleaseChannel.Stable, }, nightlyTasks: { startTime: '00:00', diff --git a/server/src/constants.ts b/server/src/constants.ts index 9f8cdbefdb..16b7731dce 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -1,7 +1,15 @@ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { SemVer } from 'semver'; -import { ApiTag, AudioCodec, DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum'; +import { + ApiTag, + AudioCodec, + DatabaseExtension, + ExifOrientation, + TranscodeHardwareAcceleration, + VectorIndex, + VideoCodec, +} from 'src/enum'; export const IMMICH_SERVER_START = 'Immich Server is listening'; @@ -202,3 +210,32 @@ export const AUDIO_ENCODER: Record = { [AudioCodec.Opus]: 'libopus', [AudioCodec.PcmS16le]: 'pcm_s16le', }; + +export const SUPPORTED_HWA_CODECS: Record = { + [TranscodeHardwareAcceleration.Nvenc]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Av1], + [TranscodeHardwareAcceleration.Qsv]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1], + [TranscodeHardwareAcceleration.Vaapi]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1], + [TranscodeHardwareAcceleration.Rkmpp]: [VideoCodec.H264, VideoCodec.Hevc], + [TranscodeHardwareAcceleration.Disabled]: [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1], +}; + +export const HLS_BACKPRESSURE_PAUSE_SEGMENTS = 30; +export const HLS_BACKPRESSURE_RESUME_SEGMENTS = 15; +export const HLS_CLEANUP_INTERVAL_MS = 60 * 1000; +export const HLS_INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; +export const HLS_LEASE_DURATION_MS = 30 * 60 * 1000; +export const HLS_PLAYLIST_CONTENT_TYPE = 'application/vnd.apple.mpegurl'; +export const HLS_SEGMENT_DURATION = 2; +export const HLS_SEGMENT_FILENAME_REGEX = /^seg_(\d+)\.m4s$/; +export const HLS_VARIANTS = [ + { resolution: 480, codec: VideoCodec.Av1, bitrate: 1_000_000, codecString: 'av01.0.04M.08' }, + { resolution: 480, codec: VideoCodec.Hevc, bitrate: 1_200_000, codecString: 'hvc1.1.6.L90.B0' }, + { resolution: 480, codec: VideoCodec.H264, bitrate: 2_500_000, codecString: 'avc1.64001e' }, + { resolution: 720, codec: VideoCodec.Av1, bitrate: 2_000_000, codecString: 'av01.0.08M.08' }, + { resolution: 720, codec: VideoCodec.Hevc, bitrate: 2_500_000, codecString: 'hvc1.1.6.L93.B0' }, + { resolution: 720, codec: VideoCodec.H264, bitrate: 5_000_000, codecString: 'avc1.64001f' }, + { resolution: 1080, codec: VideoCodec.Av1, bitrate: 4_000_000, codecString: 'av01.0.09M.08' }, + { resolution: 1080, codec: VideoCodec.Hevc, bitrate: 4_500_000, codecString: 'hvc1.1.6.L120.B0' }, + { resolution: 1080, codec: VideoCodec.H264, bitrate: 8_000_000, codecString: 'avc1.640028' }, +]; +export const HLS_VERSION = 7; diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index dc3754ce24..336ea1cf91 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -35,6 +35,7 @@ import { TimelineController } from 'src/controllers/timeline.controller'; import { TrashController } from 'src/controllers/trash.controller'; import { UserAdminController } from 'src/controllers/user-admin.controller'; import { UserController } from 'src/controllers/user.controller'; +import { VideoStreamController } from 'src/controllers/video-stream.controller'; import { ViewController } from 'src/controllers/view.controller'; import { WorkflowController } from 'src/controllers/workflow.controller'; @@ -76,6 +77,7 @@ export const controllers = [ TrashController, UserAdminController, UserController, + VideoStreamController, ViewController, WorkflowController, ]; diff --git a/server/src/controllers/video-stream.controller.ts b/server/src/controllers/video-stream.controller.ts new file mode 100644 index 0000000000..8707584361 --- /dev/null +++ b/server/src/controllers/video-stream.controller.ts @@ -0,0 +1,79 @@ +import { Controller, Delete, Get, Header, HttpCode, HttpStatus, Next, Param, Res } from '@nestjs/common'; +import { ApiProduces, ApiTags } from '@nestjs/swagger'; +import { NextFunction, Response } from 'express'; +import { HLS_PLAYLIST_CONTENT_TYPE } from 'src/constants'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { HlsSegmentParamDto, HlsSessionParamDto, HlsVariantParamDto } from 'src/dtos/streaming.dto'; +import { ApiTag, Permission, RouteKey } from 'src/enum'; +import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { HlsService } from 'src/services/hls.service'; +import { sendFile } from 'src/utils/file'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags(ApiTag.Assets) +@Controller(RouteKey.Asset) +export class VideoStreamController { + constructor( + private logger: LoggingRepository, + private service: HlsService, + ) {} + + @Get(':id/video/stream/main.m3u8') + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Header('Cache-Control', 'no-cache') + @Header('Content-Type', HLS_PLAYLIST_CONTENT_TYPE) + @ApiProduces(HLS_PLAYLIST_CONTENT_TYPE) + @Endpoint({ + summary: 'Get HLS main playlist', + description: 'Returns an HLS main playlist with all available variants for the asset.', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + getMainPlaylist(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { + return this.service.getMainPlaylist(auth, id); + } + + @Get(':id/video/stream/:sessionId/:variantIndex/playlist.m3u8') + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Header('Cache-Control', 'no-cache') + @Header('Content-Type', HLS_PLAYLIST_CONTENT_TYPE) + @ApiProduces(HLS_PLAYLIST_CONTENT_TYPE) + @Endpoint({ + summary: 'Get HLS media playlist', + description: 'Returns an HLS media playlist for one variant of the streaming session.', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + getMediaPlaylist(@Auth() auth: AuthDto, @Param() { id, sessionId }: HlsVariantParamDto) { + return this.service.getMediaPlaylist(auth, id, sessionId); + } + + @Get(':id/video/stream/:sessionId/:variantIndex/:filename') + @FileResponse() + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Endpoint({ + summary: 'Get HLS segment or init file', + description: 'Streams an HLS init segment (init.mp4) or media segment (seg_N.m4s).', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + async getSegment( + @Auth() auth: AuthDto, + @Param() { id, sessionId, variantIndex, filename }: HlsSegmentParamDto, + @Res() res: Response, + @Next() next: NextFunction, + ) { + await sendFile(res, next, () => this.service.getSegment(auth, id, sessionId, variantIndex, filename), this.logger); + } + + @Delete(':id/video/stream/:sessionId') + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.AssetView, sharedLink: true }) + @Endpoint({ + summary: 'End HLS streaming session', + description: 'Releases server resources for the streaming session.', + history: new HistoryBuilder().added('v3').alpha('v3'), + }) + async endSession(@Auth() auth: AuthDto, @Param() { id, sessionId }: HlsSessionParamDto) { + await this.service.endSession(auth, id, sessionId); + } +} diff --git a/server/src/controllers/workflow.controller.spec.ts b/server/src/controllers/workflow.controller.spec.ts index 7bc164e285..140fb00e95 100644 --- a/server/src/controllers/workflow.controller.spec.ts +++ b/server/src/controllers/workflow.controller.spec.ts @@ -1,5 +1,5 @@ +import { WorkflowTrigger } from '@immich/plugin-sdk'; import { WorkflowController } from 'src/controllers/workflow.controller'; -import { WorkflowTrigger } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { WorkflowService } from 'src/services/workflow.service'; import request from 'supertest'; diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index d40518762d..8802145020 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -35,6 +35,10 @@ export interface MoveRequest { export type ThumbnailPathEntity = { id: string; ownerId: string }; +export type HlsSessionFolder = { ownerId: string; sessionId: string }; + +export type HlsVariantFolder = { ownerId: string; sessionId: string; variantIndex: number }; + export type ImagePathOptions = { fileType: AssetFileType; format: ImageFormat | RawExtractedFormat; isEdited: boolean }; let instance: StorageCore | null; @@ -125,6 +129,14 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`); } + static getHlsSessionFolder({ ownerId, sessionId }: HlsSessionFolder) { + return StorageCore.getNestedPath(StorageFolder.EncodedVideo, ownerId, sessionId); + } + + static getHlsVariantFolder({ ownerId, sessionId, variantIndex }: HlsVariantFolder) { + return join(StorageCore.getHlsSessionFolder({ ownerId, sessionId }), variantIndex.toString()); + } + static getAndroidMotionPath(asset: ThumbnailPathEntity, uuid: string) { return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${uuid}-MP.mp4`); } diff --git a/server/src/decorators.ts b/server/src/decorators.ts index c8cf1f9221..513ae36c9f 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -265,3 +265,13 @@ export class HistoryBuilder { return this; } } + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export const extraModels: Function[] = []; + +export const ExtraModel = (): ClassDecorator => { + // eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-unsafe-function-type + return (object: Function) => { + extraModels.push(object); + }; +}; diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 095e399b96..100550659d 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -65,6 +65,8 @@ const UpdateAlbumSchema = z const GetAlbumsSchema = z .object({ + id: z.uuidv4().optional().describe('Album ID'), + name: z.string().optional().describe('Album name (exact match)'), isOwned: stringToBool .optional() .describe('Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter'), diff --git a/server/src/dtos/plugin-manifest.dto.ts b/server/src/dtos/plugin-manifest.dto.ts index c8f043fde1..b175c6e1bb 100644 --- a/server/src/dtos/plugin-manifest.dto.ts +++ b/server/src/dtos/plugin-manifest.dto.ts @@ -38,6 +38,7 @@ const PluginManifestTemplateSchema = z description: z.string().min(1).describe('Template description'), trigger: WorkflowTriggerSchema.describe('Workflow trigger'), steps: z.array(PluginManifestTemplateStepSchema).describe('Workflow steps'), + uiHints: z.array(z.string()).optional().default([]).describe('Ui hints, for example "smart-album"'), }) .meta({ id: 'PluginManifestTemplateDto' }); diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index 074321bb44..8ee695d61c 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,6 +1,7 @@ +import { WorkflowTrigger } from '@immich/plugin-sdk'; import { createZodDto } from 'nestjs-zod'; import { JsonSchemaDto } from 'src/dtos/json-schema.dto'; -import { WorkflowTrigger, WorkflowTriggerSchema, WorkflowType, WorkflowTypeSchema } from 'src/enum'; +import { WorkflowTriggerSchema, WorkflowType, WorkflowTypeSchema } from 'src/enum'; import { asPluginKey } from 'src/utils/workflow'; import z from 'zod'; @@ -58,6 +59,7 @@ const PluginTemplateResponseSchema = z description: z.string().describe('Template description'), trigger: WorkflowTriggerSchema.describe('Workflow trigger'), steps: z.array(PluginTemplateStepResponseSchema).describe('Workflow steps'), + uiHints: z.array(z.string()).describe('Ui hints, for example "smart-album"'), }) .meta({ id: 'PluginTemplateResponseDto' }); @@ -91,6 +93,7 @@ export type PluginTemplate = { config?: Record | null; enabled?: boolean; }>; + uiHints: string[]; }; export const mapTemplate = (plugin: { name: string }, template: PluginTemplate): PluginTemplateResponseDto => { @@ -104,6 +107,7 @@ export const mapTemplate = (plugin: { name: string }, template: PluginTemplate): config: step.config ?? null, enabled: step.enabled, })), + uiHints: template.uiHints ?? [], }; }; diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 7c77f36d72..03d45fab1c 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -1,5 +1,6 @@ import { createZodDto } from 'nestjs-zod'; import type { SemVer } from 'semver'; +import { ExtraModel, HistoryBuilder } from 'src/decorators'; import { isoDatetimeToDate } from 'src/validation'; import z from 'zod'; @@ -58,9 +59,15 @@ const ServerStorageResponseSchema = z const ServerVersionResponseSchema = z .object({ - major: z.int().describe('Major version number'), - minor: z.int().describe('Minor version number'), - patch: z.int().describe('Patch version number'), + major: z.int().min(0).describe('Major version number'), + minor: z.int().min(0).describe('Minor version number'), + patch: z.int().min(0).describe('Patch version number'), + prerelease: z + .int() + .min(0) + .nullable() + .meta(HistoryBuilder.v3().getExtensions()) + .describe('Pre-release version number'), }) .meta({ id: 'ServerVersionResponseDto' }); @@ -138,9 +145,30 @@ const ServerFeaturesSchema = z search: z.boolean().describe('Whether search is enabled'), email: z.boolean().describe('Whether email notifications are enabled'), ocr: z.boolean().describe('Whether OCR is enabled'), + realtimeTranscoding: z.boolean().describe('Whether real-time transcoding is enabled'), }) .meta({ id: 'ServerFeaturesDto' }); +export enum ReleaseType { + Major = 'major', + Premajor = 'premajor', + Minor = 'minor', + Preminor = 'preminor', + Patch = 'patch', + Prepatch = 'prepatch', + Prerelease = 'prerelease', +} + +const ReleaseTypeSchema = z.enum(ReleaseType).meta({ id: 'ReleaseType' }).describe('Release type'); + +const ReleaseEventV1Schema = z.object({ + isAvailable: z.boolean().describe('Whether a new version is available'), + checkedAt: z.string().describe('When the server last checked for a latest version. As an ISO timestamp'), + serverVersion: ServerVersionResponseSchema, + releaseVersion: ServerVersionResponseSchema, + type: ReleaseTypeSchema.nullable(), +}); + export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {} export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {} export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {} @@ -148,7 +176,12 @@ export class ServerStorageResponseDto extends createZodDto(ServerStorageResponse export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) { static fromSemVer(value: SemVer): z.infer { - return { major: value.major, minor: value.minor, patch: value.patch }; + return { + major: value.major, + minor: value.minor, + patch: value.patch, + prerelease: (value.prerelease[1] as number) ?? null, + }; } } @@ -159,10 +192,5 @@ export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesRe export class ServerConfigDto extends createZodDto(ServerConfigSchema) {} export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {} -export interface ReleaseNotification { - isAvailable: boolean; - /** ISO8601 */ - checkedAt: string; - serverVersion: ServerVersionResponseDto; - releaseVersion: ServerVersionResponseDto; -} +@ExtraModel() +export class ReleaseEventV1 extends createZodDto(ReleaseEventV1Schema) {} diff --git a/server/src/dtos/streaming.dto.ts b/server/src/dtos/streaming.dto.ts new file mode 100644 index 0000000000..5270e45fc2 --- /dev/null +++ b/server/src/dtos/streaming.dto.ts @@ -0,0 +1,26 @@ +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; + +const HlsSessionParamSchema = z.object({ + id: z.uuidv4(), + sessionId: z.uuidv4(), +}); + +export class HlsSessionParamDto extends createZodDto(HlsSessionParamSchema) {} + +const HlsVariantParamSchema = z.object({ + id: z.uuidv4(), + sessionId: z.uuidv4(), + variantIndex: z.coerce.number().int().min(0), +}); + +export class HlsVariantParamDto extends createZodDto(HlsVariantParamSchema) {} + +const HlsSegmentParamSchema = z.object({ + id: z.uuidv4(), + sessionId: z.uuidv4(), + variantIndex: z.coerce.number().int().min(0), + filename: z.string().regex(/^(init\.mp4|seg_\d+\.m4s)$/, { error: 'Invalid HLS segment filename' }), +}); + +export class HlsSegmentParamDto extends createZodDto(HlsSegmentParamSchema) {} diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 35ef874dfa..698939e627 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { createZodDto } from 'nestjs-zod'; +import { ExtraModel } from 'src/decorators'; import { AssetEditActionSchema } from 'src/dtos/editing.dto'; import { AlbumUserRole, @@ -17,15 +17,6 @@ import { import { isoDatetimeToDate } from 'src/validation'; import z from 'zod'; -export const extraSyncModels: Function[] = []; - -const ExtraModel = (): ClassDecorator => { - // eslint-disable-next-line unicorn/consistent-function-scoping - return (object: Function) => { - extraSyncModels.push(object); - }; -}; - const SyncUserV1Schema = z .object({ id: z.string().describe('User ID'), diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 94c1aa36b0..3b31705918 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -79,6 +79,11 @@ const SystemConfigFFmpegSchema = z accel: TranscodeHardwareAccelerationSchema, accelDecode: configBool.describe('Accelerated decode'), tonemap: ToneMappingSchema, + realtime: z + .object({ + enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'), + }) + .meta({ id: 'SystemConfigFFmpegRealtimeDto' }), }) .meta({ id: 'SystemConfigFFmpegDto' }); @@ -151,8 +156,15 @@ const SystemConfigMapSchema = z }) .meta({ id: 'SystemConfigMapDto' }); +export enum ReleaseChannel { + Stable = 'stable', + ReleaseCandidate = 'releaseCandidate', +} + +const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' }); + const SystemConfigNewVersionCheckSchema = z - .object({ enabled: configBool.describe('Enabled') }) + .object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema }) .meta({ id: 'SystemConfigNewVersionCheckDto' }); const SystemConfigNightlyTasksSchema = z diff --git a/server/src/dtos/workflow.dto.ts b/server/src/dtos/workflow.dto.ts index 8a5960470d..1a2c2ac9f9 100644 --- a/server/src/dtos/workflow.dto.ts +++ b/server/src/dtos/workflow.dto.ts @@ -1,6 +1,6 @@ -import type { WorkflowStepConfig } from '@immich/plugin-sdk'; +import type { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk'; import { createZodDto } from 'nestjs-zod'; -import { WorkflowTrigger, WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum'; +import { WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum'; import z from 'zod'; const WorkflowTriggerResponseSchema = z diff --git a/server/src/enum.ts b/server/src/enum.ts index bc52e65f83..9dee1db313 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -1,3 +1,4 @@ +import { WorkflowTrigger } from '@immich/plugin-sdk'; import z from 'zod'; export enum AuthType { @@ -452,11 +453,7 @@ export enum VideoCodec { export const VideoCodecSchema = z.enum(VideoCodec).describe('Target video codec').meta({ id: 'VideoCodec' }); -export enum VideoSegmentCodec { - Av1 = 'av1', - Hevc = 'hevc', - H264 = 'h264', -} +export type VideoSegmentCodec = VideoCodec.Av1 | VideoCodec.Hevc | VideoCodec.H264; export enum AudioCodec { Mp3 = 'mp3', @@ -826,6 +823,8 @@ export enum JobName { LibrarySyncFiles = 'LibrarySyncFiles', LibraryScanQueueAll = 'LibraryScanQueueAll', + HlsSessionCleanup = 'HlsSessionCleanup', + MemoryCleanup = 'MemoryCleanup', MemoryGenerate = 'MemoryGenerate', @@ -866,7 +865,7 @@ export enum JobName { Ocr = 'Ocr', // Workflow - WorkflowAssetCreate = 'WorkflowAssetCreate', + WorkflowAssetTrigger = 'WorkflowAssetTrigger', } export const JobNameSchema = z.enum(JobName).describe('Job name').meta({ id: 'JobName' }); @@ -919,6 +918,7 @@ export enum DatabaseLock { MaintenanceOperation = 621, MemoryCreation = 777, VersionCheck = 800, + HlsSessionCleanup = 850, } export enum MaintenanceAction { @@ -1164,11 +1164,6 @@ export enum PluginContext { export const PluginContextSchema = z.enum(PluginContext).describe('Plugin context').meta({ id: 'PluginContextType' }); -export enum WorkflowTrigger { - AssetCreate = 'AssetCreate', - PersonRecognized = 'PersonRecognized', -} - export const WorkflowTriggerSchema = z .enum(WorkflowTrigger) .describe('Plugin trigger type') diff --git a/server/src/queries/video.stream.repository.sql b/server/src/queries/video.stream.repository.sql index c77882d77d..714e138ce8 100644 --- a/server/src/queries/video.stream.repository.sql +++ b/server/src/queries/video.stream.repository.sql @@ -7,6 +7,7 @@ from "video_stream_session" where "id" = $1 + and "expiresAt" > $2 -- VideoStreamRepository.getVariant select @@ -27,11 +28,13 @@ where -- VideoStreamRepository.getExpiredSessions select - "id" + "video_stream_session"."id", + "asset"."ownerId" from "video_stream_session" + inner join "asset" on "asset"."id" = "video_stream_session"."assetId" where - "expiresAt" <= $1 + "video_stream_session"."expiresAt" <= $1 -- VideoStreamRepository.extendSession update "video_stream_session" @@ -44,3 +47,253 @@ where delete from "video_stream_session" where "id" = $1 + +-- VideoStreamRepository.getForMainPlaylist +select + ( + select + to_json(obj) + from + ( + select + "asset_video"."index", + "asset_video"."codecName", + "asset_video"."profile", + "asset_video"."level", + "asset_video"."bitrate", + "asset_exif"."exifImageWidth" as "width", + "asset_exif"."exifImageHeight" as "height", + "asset_video"."pixelFormat", + "asset_video"."frameCount", + "asset_exif"."fps" as "frameRate", + "asset_video"."timeBase", + case + when "asset_exif"."orientation" = '6' then -90 + when "asset_exif"."orientation" = '8' then 90 + when "asset_exif"."orientation" = '3' then 180 + else 0 + end as "rotation", + "asset_video"."colorPrimaries", + "asset_video"."colorMatrix", + "asset_video"."colorTransfer", + "asset_video"."dvProfile", + "asset_video"."dvLevel", + "asset_video"."dvBlSignalCompatibilityId" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "videoStream", + ( + select + to_json(obj) + from + ( + select + "asset_keyframe"."pts" as "keyframePts", + "asset_keyframe"."accDuration" as "keyframeAccDuration", + "asset_keyframe"."ownDuration" as "keyframeOwnDuration", + "asset_keyframe"."totalDuration", + "asset_keyframe"."packetCount", + "asset_keyframe"."outputFrames" + from + ( + select + 1 + ) as "dummy" + where + "asset_keyframe"."assetId" is not null + ) as obj + ) as "packets" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "asset_video" on "asset"."id" = "asset_video"."assetId" + inner join "asset_keyframe" on "asset"."id" = "asset_keyframe"."assetId" +where + "asset"."id" = $1 + +-- VideoStreamRepository.getForMediaPlaylist +select + ( + select + to_json(obj) + from + ( + select + "asset_video"."index", + "asset_video"."codecName", + "asset_video"."profile", + "asset_video"."level", + "asset_video"."bitrate", + "asset_exif"."exifImageWidth" as "width", + "asset_exif"."exifImageHeight" as "height", + "asset_video"."pixelFormat", + "asset_video"."frameCount", + "asset_exif"."fps" as "frameRate", + "asset_video"."timeBase", + case + when "asset_exif"."orientation" = '6' then -90 + when "asset_exif"."orientation" = '8' then 90 + when "asset_exif"."orientation" = '3' then 180 + else 0 + end as "rotation", + "asset_video"."colorPrimaries", + "asset_video"."colorMatrix", + "asset_video"."colorTransfer", + "asset_video"."dvProfile", + "asset_video"."dvLevel", + "asset_video"."dvBlSignalCompatibilityId" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "videoStream", + ( + select + to_json(obj) + from + ( + select + "asset_keyframe"."pts" as "keyframePts", + "asset_keyframe"."accDuration" as "keyframeAccDuration", + "asset_keyframe"."ownDuration" as "keyframeOwnDuration", + "asset_keyframe"."totalDuration", + "asset_keyframe"."packetCount", + "asset_keyframe"."outputFrames" + from + ( + select + 1 + ) as "dummy" + where + "asset_keyframe"."assetId" is not null + ) as obj + ) as "packets" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "video_stream_session" on "asset"."id" = "video_stream_session"."assetId" + inner join "asset_video" on "asset"."id" = "asset_video"."assetId" + inner join "asset_keyframe" on "asset"."id" = "asset_keyframe"."assetId" +where + "asset"."id" = $1 + and "video_stream_session"."id" = $2 + and "video_stream_session"."expiresAt" > $3 + +-- VideoStreamRepository.getForTranscoding +select + "asset"."originalPath", + ( + select + to_json(obj) + from + ( + select + "asset_audio"."index", + "asset_audio"."codecName", + "asset_audio"."profile", + "asset_audio"."bitrate" + from + ( + select + 1 + ) as "dummy" + where + "asset_audio"."assetId" is not null + ) as obj + ) as "audioStream", + ( + select + to_json(obj) + from + ( + select + "asset_video"."index", + "asset_video"."codecName", + "asset_video"."profile", + "asset_video"."level", + "asset_video"."bitrate", + "asset_exif"."exifImageWidth" as "width", + "asset_exif"."exifImageHeight" as "height", + "asset_video"."pixelFormat", + "asset_video"."frameCount", + "asset_exif"."fps" as "frameRate", + "asset_video"."timeBase", + case + when "asset_exif"."orientation" = '6' then -90 + when "asset_exif"."orientation" = '8' then 90 + when "asset_exif"."orientation" = '3' then 180 + else 0 + end as "rotation", + "asset_video"."colorPrimaries", + "asset_video"."colorMatrix", + "asset_video"."colorTransfer", + "asset_video"."dvProfile", + "asset_video"."dvLevel", + "asset_video"."dvBlSignalCompatibilityId" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "videoStream", + ( + select + to_json(obj) + from + ( + select + "asset_video"."formatName", + "asset_video"."formatLongName", + "asset"."duration", + "asset_video"."bitrate" + from + ( + select + 1 + ) as "dummy" + where + "asset_video"."assetId" is not null + ) as obj + ) as "format", + ( + select + to_json(obj) + from + ( + select + "asset_keyframe"."pts" as "keyframePts", + "asset_keyframe"."accDuration" as "keyframeAccDuration", + "asset_keyframe"."ownDuration" as "keyframeOwnDuration", + "asset_keyframe"."totalDuration", + "asset_keyframe"."packetCount", + "asset_keyframe"."outputFrames" + from + ( + select + 1 + ) as "dummy" + where + "asset_keyframe"."assetId" is not null + ) as obj + ) as "packets" +from + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + left join "asset_audio" on "asset"."id" = "asset_audio"."assetId" + inner join "asset_video" on "asset"."id" = "asset_video"."assetId" + inner join "asset_keyframe" on "asset"."id" = "asset_keyframe"."assetId" +where + "asset"."id" = $1 diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index a712151355..724788fa74 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -209,11 +209,16 @@ export class AlbumRepository { } @GenerateSql({ params: [DummyValue.UUID, { isOwned: true, isShared: true }] }) - getAll(ownerId: string, options: { isOwned?: boolean; isShared?: boolean } = {}): Promise { + getAll( + ownerId: string, + options: { id?: string; isOwned?: boolean; isShared?: boolean; name?: string } = {}, + ): Promise { return this.buildAlbumBaseQuery(ownerId, options) .selectAll('album') .select(withAlbumUsers(ownerId)) .select(withSharedLink) + .$if(!!options.id, (qb) => qb.where('album.id', '=', options.id!)) + .$if(!!options.name, (qb) => qb.where('album.albumName', '=', options.name!)) .orderBy('album.createdAt', 'desc') .execute(); } diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index fa92a6b0b7..b4d968599b 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -92,6 +92,14 @@ type EventMap = { AuthChangePassword: [{ userId: string; currentSessionId?: string; invalidateSessions?: boolean }]; + // hls streaming events + HlsSegmentRequest: [{ sessionId: string; assetId: string; variantIndex: number; segmentIndex: number }]; + HlsSegmentResult: [{ sessionId: string; variantIndex: number; segmentIndex: number; error?: string }]; + HlsHeartbeat: [{ sessionId: string; variantIndex?: number; segmentIndex?: number }]; + HlsSessionRequest: [{ sessionId: string; assetId: string; ownerId: string }]; + HlsSessionResult: [{ sessionId: string; error?: string }]; + HlsSessionEnd: [{ sessionId: string }]; + // websocket events WebsocketConnect: [{ userId: string }]; }; diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index fa08ba8701..c2ec95636a 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -490,18 +490,43 @@ export class MediaRepository { return this.parseInt(b.bit_rate) - this.parseInt(a.bit_rate); } + /* Ported from https://code.ffmpeg.org/FFmpeg/FFmpeg/src/commit/5c44245878e235ae64fe87fb9877644856d33d1d/fftools/ffmpeg_filter.c + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright (c) FFmpeg authors and contributors — https://ffmpeg.org/ + * Modifications: TS port operating on probe-derived packet metadata rather than decoded AVFrames. */ private cfrOutputFrames(packets: { pts: number; duration: number }[], slotsPerTick: number) { - // Packets may be out of PTS order due to B-frames packets.sort((a, b) => a.pts - b.pts); const firstPts = packets[0].pts; let outputFrames = 0; let nextPts = 0; + const history = [0, 0, 0]; for (const pkt of packets) { - const delta = (pkt.pts - firstPts) * slotsPerTick - nextPts + pkt.duration * slotsPerTick; - const nb = delta < -1.1 ? 0 : delta > 1.1 ? Math.round(delta) : 1; + const syncIpts = (pkt.pts - firstPts) * slotsPerTick; + const duration = pkt.duration * slotsPerTick; + let delta0 = syncIpts - nextPts; + const delta = delta0 + duration; + + if (delta0 < 0 && delta > 0) { + delta0 = 0; + } + + let nb = 1; + let nbPrev = 0; + if (delta < -1.1) { + nb = 0; + } else if (delta > 1.1) { + nb = Math.round(delta); + if (delta0 > 1.1) { + nbPrev = Math.round(delta0 - 0.6); + } + } outputFrames += nb; nextPts += nb; + history[2] = history[1]; + history[1] = history[0]; + history[0] = nbPrev; } - return outputFrames; + const median = history.sort((a, b) => a - b)[1]; + return outputFrames + median; } } diff --git a/server/src/repositories/process.repository.ts b/server/src/repositories/process.repository.ts index 9d8cac1f40..928531408f 100644 --- a/server/src/repositories/process.repository.ts +++ b/server/src/repositories/process.repository.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { ChildProcessWithoutNullStreams, fork, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; +import { fork, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; import { Duplex } from 'node:stream'; @Injectable() export class ProcessRepository { - spawn(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams { - return spawn(command, args, options); - } + spawn = spawn; spawnDuplexStream(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): Duplex { let stdinClosed = false; diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index 85d26d6cfa..c8fb896281 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -4,6 +4,7 @@ import { exec as execCallback } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -64,10 +65,12 @@ export class ServerInfoRepository { this.logger.setContext(ServerInfoRepository.name); } - async getLatestRelease(): Promise { + async getLatestRelease(channel: ReleaseChannel): Promise { try { const { versionCheck } = this.configRepository.getEnv(); - const response = await fetch(versionCheck.url); + const url = new URL(versionCheck.url); + url.searchParams.append('channel', channel); + const response = await fetch(url); if (!response.ok) { throw new Error(`Version check request failed with status ${response.status}: ${await response.text()}`); diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 1d3971fd28..9604372fbe 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -10,6 +10,7 @@ import { existsSync, mkdirSync, ReadOptionsWithBuffer, + watch, } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -277,6 +278,8 @@ export class StorageRepository { return () => watcher.close(); } + watchDir = watch; // Native fs.watch without chokidar overhead + private asGlob(pathToCrawl: string): string { const escapedPath = escapePath(pathToCrawl).replaceAll('"', '["]').replaceAll("'", "[']").replaceAll('`', '[`]'); const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`; diff --git a/server/src/repositories/video-stream.repository.ts b/server/src/repositories/video-stream.repository.ts index e23ee4ca4c..43c5ef80f0 100644 --- a/server/src/repositories/video-stream.repository.ts +++ b/server/src/repositories/video-stream.repository.ts @@ -8,6 +8,7 @@ import { VideoStreamSessionTable, VideoStreamVariantTable, } from 'src/schema/tables/video-stream.table'; +import { withAudioStream, withVideoFormat, withVideoPackets, withVideoStream } from 'src/utils/database'; @Injectable() export class VideoStreamRepository { @@ -27,7 +28,12 @@ export class VideoStreamRepository { @GenerateSql({ params: [DummyValue.UUID] }) getSession(id: string) { - return this.db.selectFrom('video_stream_session').selectAll().where('id', '=', id).executeTakeFirst(); + return this.db + .selectFrom('video_stream_session') + .selectAll() + .where('id', '=', id) + .where('expiresAt', '>', new Date()) + .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID] }) @@ -47,7 +53,12 @@ export class VideoStreamRepository { @GenerateSql() getExpiredSessions() { - return this.db.selectFrom('video_stream_session').select(['id']).where('expiresAt', '<=', new Date()).execute(); + return this.db + .selectFrom('video_stream_session') + .innerJoin('asset', 'asset.id', 'video_stream_session.assetId') + .select(['video_stream_session.id', 'asset.ownerId']) + .where('video_stream_session.expiresAt', '<=', new Date()) + .execute(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.DATE] }) @@ -59,4 +70,50 @@ export class VideoStreamRepository { async deleteSession(id: string) { await this.db.deleteFrom('video_stream_session').where('id', '=', id).execute(); } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForMainPlaylist(id: string) { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset.id', '=', id) + .innerJoin('asset_video', 'asset.id', 'asset_video.assetId') + .innerJoin('asset_keyframe', 'asset.id', 'asset_keyframe.assetId') + .select((eb) => withVideoStream(eb).$notNull().as('videoStream')) + .select((eb) => withVideoPackets(eb).$notNull().as('packets')) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) + async getForMediaPlaylist(id: string, sessionId: string) { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .innerJoin('video_stream_session', 'asset.id', 'video_stream_session.assetId') + .where('asset.id', '=', id) + .where('video_stream_session.id', '=', sessionId) + .where('video_stream_session.expiresAt', '>', new Date()) + .innerJoin('asset_video', 'asset.id', 'asset_video.assetId') + .innerJoin('asset_keyframe', 'asset.id', 'asset_keyframe.assetId') + .select((eb) => withVideoStream(eb).$notNull().as('videoStream')) + .select((eb) => withVideoPackets(eb).$notNull().as('packets')) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForTranscoding(id: string) { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset.id', '=', id) + .leftJoin('asset_audio', 'asset.id', 'asset_audio.assetId') + .innerJoin('asset_video', 'asset.id', 'asset_video.assetId') + .innerJoin('asset_keyframe', 'asset.id', 'asset_keyframe.assetId') + .select('asset.originalPath') + .select((eb) => withAudioStream(eb).as('audioStream')) + .select((eb) => withVideoStream(eb).$notNull().as('videoStream')) + .select((eb) => withVideoFormat(eb).$notNull().as('format')) + .select((eb) => withVideoPackets(eb).$notNull().as('packets')) + .executeTakeFirst(); + } } diff --git a/server/src/repositories/websocket.repository.ts b/server/src/repositories/websocket.repository.ts index b4a0fcc00a..d79e1563e3 100644 --- a/server/src/repositories/websocket.repository.ts +++ b/server/src/repositories/websocket.repository.ts @@ -10,13 +10,22 @@ import { Server, Socket } from 'socket.io'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { NotificationDto } from 'src/dtos/notification.dto'; -import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ReleaseEventV1, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto'; import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { handlePromiseError } from 'src/utils/misc'; -export const serverEvents = ['ConfigUpdate', 'AppRestart'] as const; +export const serverEvents = [ + 'ConfigUpdate', + 'AppRestart', + 'HlsSegmentRequest', + 'HlsSegmentResult', + 'HlsHeartbeat', + 'HlsSessionRequest', + 'HlsSessionResult', + 'HlsSessionEnd', +] as const; export type ServerEvents = (typeof serverEvents)[number]; export interface ClientEventMap { @@ -31,7 +40,7 @@ export interface ClientEventMap { on_person_thumbnail: [string]; on_server_version: [ServerVersionResponseDto]; on_config_update: []; - on_new_release: [ReleaseNotification]; + on_new_release: [ReleaseEventV1]; on_notification: [NotificationDto]; on_session_delete: [string]; diff --git a/server/src/repositories/workflow.repository.ts b/server/src/repositories/workflow.repository.ts index 69ecb83ae9..9ceef72a50 100644 --- a/server/src/repositories/workflow.repository.ts +++ b/server/src/repositories/workflow.repository.ts @@ -45,10 +45,10 @@ export class WorkflowRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - search(dto: WorkflowSearchDto & { ownerId?: string }) { + search(dto: WorkflowSearchDto & { userId?: string }) { return this.queryBuilder() .$if(!!dto.id, (qb) => qb.where('id', '=', dto.id!)) - .$if(!!dto.ownerId, (qb) => qb.where('ownerId', '=', dto.ownerId!)) + .$if(!!dto.userId, (qb) => qb.where('ownerId', '=', dto.userId!)) .$if(!!dto.trigger, (qb) => qb.where('trigger', '=', dto.trigger!)) .$if(dto.enabled !== undefined, (qb) => qb.where('enabled', '=', dto.enabled!)) .orderBy('createdAt', 'desc') @@ -103,6 +103,10 @@ export class WorkflowRepository { }); } + async updateStep(id: string, dto: Updateable) { + await this.db.updateTable('workflow_step').where('workflow_step.id', '=', id).set(dto).execute(); + } + private async replaceAndReturn(tx: Kysely, workflowId: string, steps?: WorkflowStepUpsert[]) { if (steps) { await tx.deleteFrom('workflow_step').where('workflowId', '=', workflowId).execute(); diff --git a/server/src/schema/enums.ts b/server/src/schema/enums.ts index 73f8133441..ecf559c39d 100644 --- a/server/src/schema/enums.ts +++ b/server/src/schema/enums.ts @@ -1,12 +1,5 @@ import { registerEnum } from '@immich/sql-tools'; -import { - AlbumUserRole, - AssetStatus, - AssetVisibility, - ChecksumAlgorithm, - SourceType, - VideoSegmentCodec, -} from 'src/enum'; +import { AlbumUserRole, AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType, VideoCodec } from 'src/enum'; export const album_user_role_enum = registerEnum({ name: 'album_user_role_enum', @@ -35,5 +28,5 @@ export const asset_checksum_algorithm_enum = registerEnum({ export const video_stream_variant_codec_enum = registerEnum({ name: 'video_stream_variant_codec_enum', - values: Object.values(VideoSegmentCodec), + values: [VideoCodec.Av1, VideoCodec.Hevc, VideoCodec.H264], }); diff --git a/server/src/schema/tables/workflow.table.ts b/server/src/schema/tables/workflow.table.ts index 8ac89d4b65..944fffd9d5 100644 --- a/server/src/schema/tables/workflow.table.ts +++ b/server/src/schema/tables/workflow.table.ts @@ -1,3 +1,4 @@ +import { WorkflowTrigger } from '@immich/plugin-sdk'; import { Column, CreateDateColumn, @@ -9,7 +10,6 @@ import { UpdateDateColumn, } from '@immich/sql-tools'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { WorkflowTrigger } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; @Table('workflow') diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 723288e5b5..31c4ff2e38 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -37,15 +37,12 @@ export class AlbumService extends BaseService { }; } - async getAll( - { user: { id: ownerId } }: AuthDto, - { assetId, isOwned, isShared }: GetAlbumsDto, - ): Promise { + async getAll({ user: { id: ownerId } }: AuthDto, { assetId, ...rest }: GetAlbumsDto): Promise { await this.albumRepository.updateThumbnails(); const albums = assetId ? await this.albumRepository.getByAssetId(ownerId, assetId) - : await this.albumRepository.getAll(ownerId, { isOwned, isShared }); + : await this.albumRepository.getAll(ownerId, rest); if (albums.length === 0) { return []; diff --git a/server/src/services/hls.service.spec.ts b/server/src/services/hls.service.spec.ts new file mode 100644 index 0000000000..ccbba48107 --- /dev/null +++ b/server/src/services/hls.service.spec.ts @@ -0,0 +1,327 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TranscodeHardwareAcceleration } from 'src/enum'; +import { HlsService } from 'src/services/hls.service'; +import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub'; +import { factory } from 'test/small.factory'; +import { newTestService, ServiceMocks } from 'test/utils'; + +// EXTINF values come from FFmpeg's playlist to enforce an exact match +const eiffelExpectedMediaPlaylist = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="init.mp4" +#EXTINF:2.007222, +seg_0.m4s +#EXTINF:2.007222, +seg_1.m4s +#EXTINF:2.007222, +seg_2.m4s +#EXTINF:2.007222, +seg_3.m4s +#EXTINF:2.007222, +seg_4.m4s +#EXTINF:2.007222, +seg_5.m4s +#EXTINF:2.007222, +seg_6.m4s +#EXTINF:2.007222, +seg_7.m4s +#EXTINF:2.007222, +seg_8.m4s +#EXTINF:2.007222, +seg_9.m4s +#EXTINF:2.007222, +seg_10.m4s +#EXTINF:0.281011, +seg_11.m4s +#EXT-X-ENDLIST +`; + +const waterfallExpectedMediaPlaylist = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="init.mp4" +#EXTINF:2.011405, +seg_0.m4s +#EXTINF:2.011405, +seg_1.m4s +#EXTINF:2.011405, +seg_2.m4s +#EXTINF:2.011405, +seg_3.m4s +#EXTINF:2.011405, +seg_4.m4s +#EXTINF:0.301711, +seg_5.m4s +#EXT-X-ENDLIST +`; + +const trainExpectedMediaPlaylist = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:2 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="init.mp4" +#EXTINF:2.000000, +seg_0.m4s +#EXTINF:2.000000, +seg_1.m4s +#EXTINF:2.000000, +seg_2.m4s +#EXTINF:2.000000, +seg_3.m4s +#EXTINF:2.000000, +seg_4.m4s +#EXTINF:2.000000, +seg_5.m4s +#EXTINF:2.000000, +seg_6.m4s +#EXTINF:2.000000, +seg_7.m4s +#EXTINF:2.000000, +seg_8.m4s +#EXTINF:2.000000, +seg_9.m4s +#EXTINF:1.733333, +seg_10.m4s +#EXT-X-ENDLIST +`; + +const sessionId = '00000000-0000-0000-0000-000000000000'; + +const eiffelExpectedMasterDisabled = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/0/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/1/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/2/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/3/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/4/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/5/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/6/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/7/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/8/playlist.m3u8 +`; + +const eiffelExpectedMasterRkmpp = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/1/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/2/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/4/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/5/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/7/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=24.910 +${sessionId}/8/playlist.m3u8 +`; + +const waterfallExpectedMasterDisabled = `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=480x852,CODECS="av01.0.04M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/0/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=480x852,CODECS="hvc1.1.6.L90.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/1/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=480x852,CODECS="avc1.64001e,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/2/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=720x1280,CODECS="av01.0.08M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/3/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=720x1280,CODECS="hvc1.1.6.L93.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/4/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=720x1280,CODECS="avc1.64001f,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/5/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1080x1920,CODECS="av01.0.09M.08,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/6/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=4500000,RESOLUTION=1080x1920,CODECS="hvc1.1.6.L120.B0,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/7/playlist.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=29.830 +${sessionId}/8/playlist.m3u8 +`; + +describe(HlsService.name, () => { + let sut: HlsService; + let mocks: ServiceMocks; + + beforeEach(() => { + ({ sut, mocks } = newTestService(HlsService)); + }); + + describe('getMainPlaylist', () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + + const setup = (asset: typeof eiffelTower | typeof waterfall, accel: TranscodeHardwareAcceleration) => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true }, accel } }); + mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset); + mocks.crypto.randomUUID.mockReturnValue(sessionId); + mocks.websocket.serverSend.mockImplementation((event, ...rest) => { + if (event === 'HlsSessionRequest') { + const { sessionId: id } = rest[0] as { sessionId: string }; + queueMicrotask(() => sut.onSessionResult({ sessionId: id })); + } + }); + }; + + it('returns main playlist for eiffel-tower (1080p portrait, no acceleration)', async () => { + setup(eiffelTower, TranscodeHardwareAcceleration.Disabled); + await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterDisabled); + }); + + it('returns main playlist for eiffel-tower with RKMPP (no AV1 variants)', async () => { + setup(eiffelTower, TranscodeHardwareAcceleration.Rkmpp); + await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(eiffelExpectedMasterRkmpp); + }); + + it('returns main playlist for waterfall (4K landscape) with no acceleration', async () => { + setup(waterfall, TranscodeHardwareAcceleration.Disabled); + await expect(sut.getMainPlaylist(auth, assetId)).resolves.toBe(waterfallExpectedMasterDisabled); + }); + + it('throws BadRequestException when realtime transcoding is disabled', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: false } } }); + await expect(sut.getMainPlaylist(auth, assetId)).rejects.toBeInstanceOf(BadRequestException); + }); + + it('throws NotFoundException when asset is not yet ready for streaming', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true } } }); + await expect(sut.getMainPlaylist(auth, assetId)).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getMediaPlaylist', () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + const fixtures = [ + { data: eiffelTower, playlist: eiffelExpectedMediaPlaylist }, + { data: waterfall, playlist: waterfallExpectedMediaPlaylist }, + { data: train, playlist: trainExpectedMediaPlaylist }, + ]; + + it.each(fixtures)('matches FFmpeg for $data.originalPath', async ({ data, playlist }) => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.videoStream.getForMediaPlaylist.mockResolvedValue(data); + await expect(sut.getMediaPlaylist(auth, assetId, sessionId)).resolves.toBe(playlist); + }); + + it('throws NotFoundException when the session/asset cannot be loaded', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + await expect(sut.getMediaPlaylist(auth, assetId, sessionId)).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getSegment', () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + const variantIndex = 0; + + beforeEach(() => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + mocks.videoStream.getSession.mockResolvedValue({ id: sessionId, assetId } as never); + mocks.storage.checkFileExists.mockResolvedValue(true); + }); + + it('emits HlsHeartbeat with segmentIndex 0 for the first init.mp4 request', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 0, + }); + }); + + it('emits HlsHeartbeat with the parsed segment number for seg_K.m4s', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 5, + }); + }); + + it('returns lastRequested + 1 for init.mp4 after a segment has been served', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s'); + mocks.websocket.serverSend.mockClear(); + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 6, + }); + }); + + it('updates lastRequested on a backward-seek segment request', async () => { + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_5.m4s'); + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'seg_3.m4s'); + mocks.websocket.serverSend.mockClear(); + await sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId, + variantIndex, + segmentIndex: 4, + }); + }); + + it('tracks segment state per session independently', async () => { + await sut.getSegment(auth, assetId, 'session-a', variantIndex, 'seg_5.m4s'); + await sut.getSegment(auth, assetId, 'session-b', variantIndex, 'seg_2.m4s'); + mocks.websocket.serverSend.mockClear(); + await sut.getSegment(auth, assetId, 'session-a', variantIndex, 'init.mp4'); + await sut.getSegment(auth, assetId, 'session-b', variantIndex, 'init.mp4'); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId: 'session-a', + variantIndex, + segmentIndex: 6, + }); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsHeartbeat', { + sessionId: 'session-b', + variantIndex, + segmentIndex: 3, + }); + }); + + it('rejects pending waiters for the previous variant on variant change', async () => { + mocks.storage.checkFileExists.mockResolvedValueOnce(false); + + const pending = sut.getSegment(auth, assetId, sessionId, 0, 'seg_1.m4s'); + await new Promise((resolve) => setImmediate(resolve)); + await sut.getSegment(auth, assetId, sessionId, 1, 'seg_1.m4s'); + + await expect(pending).rejects.toThrow('Variant changed'); + }); + + it('throws NotFoundException when the session does not exist', async () => { + mocks.videoStream.getSession.mockReset(); + await expect(sut.getSegment(auth, assetId, sessionId, variantIndex, 'init.mp4')).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('endSession', () => { + it('emits HlsSessionEnd', async () => { + const auth = factory.auth(); + const assetId = 'asset-1'; + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + await sut.endSession(auth, assetId, sessionId); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionEnd', { sessionId }); + }); + }); +}); diff --git a/server/src/services/hls.service.ts b/server/src/services/hls.service.ts new file mode 100644 index 0000000000..fba8b8e060 --- /dev/null +++ b/server/src/services/hls.service.ts @@ -0,0 +1,198 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { constants } from 'node:fs'; +import { join } from 'node:path'; +import { + HLS_SEGMENT_DURATION, + HLS_SEGMENT_FILENAME_REGEX, + HLS_VARIANTS, + HLS_VERSION, + SUPPORTED_HWA_CODECS, +} from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { OnEvent } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { CacheControl, ImmichWorker, Permission } from 'src/enum'; +import { ArgOf } from 'src/repositories/event.repository'; +import { BaseService } from 'src/services/base.service'; +import { VideoPacketInfo, VideoStreamInfo } from 'src/types'; +import { PendingEvents } from 'src/utils/event'; +import { ImmichFileResponse } from 'src/utils/file'; +import { getOutputSize } from 'src/utils/media'; + +type AssetWithStreamInfo = { videoStream: VideoStreamInfo & { timeBase: number }; packets: VideoPacketInfo }; +type ApiSession = { lastRequestedSegment: number | null; lastVariantIndex: number | null }; + +@Injectable() +export class HlsService extends BaseService { + private pendingSegments = new PendingEvents<'HlsSegmentResult'>({ timeoutMs: 15_000 }); + private pendingSessions = new PendingEvents<'HlsSessionResult'>({ timeoutMs: 5000 }); + private sessions = new Map(); + + @OnEvent({ name: 'HlsSessionResult', server: true, workers: [ImmichWorker.Api] }) + onSessionResult(event: ArgOf<'HlsSessionResult'>) { + this.pendingSessions.complete(event.sessionId, event); + if (event.error) { + this.sessions.delete(event.sessionId); + this.pendingSegments.rejectByPrefix(`${event.sessionId}:`, event.error); + } + } + + @OnEvent({ name: 'HlsSessionEnd', server: true, workers: [ImmichWorker.Api] }) + onSessionEnd({ sessionId }: ArgOf<'HlsSessionEnd'>) { + this.sessions.delete(sessionId); + this.pendingSegments.rejectByPrefix(`${sessionId}:`, 'Session ended'); + } + + @OnEvent({ name: 'HlsSegmentResult', server: true, workers: [ImmichWorker.Api] }) + onSegmentResult(event: ArgOf<'HlsSegmentResult'>) { + this.pendingSegments.complete(this.getSegmentKey(event), event); + } + + async getMainPlaylist(auth: AuthDto, assetId: string) { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + const { ffmpeg } = await this.getConfig({ withCache: true }); + if (!ffmpeg.realtime.enabled) { + throw new BadRequestException('Real-time transcoding is not enabled'); + } + + const asset = await this.videoStreamRepository.getForMainPlaylist(assetId); + if (!asset) { + throw new NotFoundException('Asset is not yet ready for streaming'); + } + + // Sharing the sessionId allows only one microservices worker to successfully insert to the session table. + // The microservices worker that creates a session owns the transcoding lifecycle for it. + const sessionId = this.cryptoRepository.randomUUID(); + this.websocketRepository.serverSend('HlsSessionRequest', { sessionId, assetId, ownerId: auth.user.id }); + await this.pendingSessions.wait(sessionId); + this.trackSession(sessionId); + + return this.generateMainPlaylist(sessionId, ffmpeg, asset); + } + + async getMediaPlaylist(auth: AuthDto, assetId: string, sessionId: string) { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + + const asset = await this.videoStreamRepository.getForMediaPlaylist(assetId, sessionId); + if (!asset) { + throw new NotFoundException('Asset not found or not yet ready for streaming'); + } + + return this.generateMediaPlaylist(asset); + } + + async getSegment(auth: AuthDto, assetId: string, sessionId: string, variantIndex: number, filename: string) { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + + const session = await this.videoStreamRepository.getSession(sessionId); + if (!session) { + throw new NotFoundException('Session not found'); + } + + const variantDir = StorageCore.getHlsVariantFolder({ ownerId: auth.user.id, sessionId, variantIndex }); + const path = join(variantDir, filename); + const response = new ImmichFileResponse({ + path, + contentType: 'video/mp4', + cacheControl: CacheControl.PrivateWithCache, + }); + + const apiSession = this.trackSession(sessionId, variantIndex); + const segmentIndex = this.getSegmentIndex(apiSession, filename); + this.websocketRepository.serverSend('HlsHeartbeat', { sessionId, variantIndex, segmentIndex }); + + if (await this.storageRepository.checkFileExists(path, constants.R_OK)) { + return response; + } + + this.websocketRepository.serverSend('HlsSegmentRequest', { sessionId, assetId, variantIndex, segmentIndex }); + await this.pendingSegments.wait(this.getSegmentKey({ sessionId, variantIndex, segmentIndex })); + + return response; + } + + async endSession(auth: AuthDto, assetId: string, sessionId: string): Promise { + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] }); + + this.websocketRepository.serverSend('HlsSessionEnd', { sessionId }); + } + + private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) { + const fps = ((asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration).toFixed(3); + const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width); + const targetResolution = Math.max(sourceResolution, HLS_VARIANTS[0].resolution); + const lines = ['#EXTM3U', `#EXT-X-VERSION:${HLS_VERSION}`]; + for (let i = 0; i < HLS_VARIANTS.length; i++) { + const { resolution, bitrate, codec, codecString } = HLS_VARIANTS[i]; + if (resolution > targetResolution || !SUPPORTED_HWA_CODECS[ffmpeg.accel].includes(codec)) { + continue; + } + const { width, height } = getOutputSize(asset.videoStream, resolution); + lines.push( + `#EXT-X-STREAM-INF:BANDWIDTH=${bitrate},RESOLUTION=${width}x${height},CODECS="${codecString},mp4a.40.2",VIDEO-RANGE=SDR,FRAME-RATE=${fps}`, + `${sessionId}/${i}/playlist.m3u8`, + ); + } + lines.push(''); + + if (lines.length === 3) { + throw new NotFoundException('No supported variants for this video'); + } + + return lines.join('\n'); + } + + private generateMediaPlaylist({ videoStream, packets }: AssetWithStreamInfo) { + const fps = (packets.packetCount * videoStream.timeBase) / packets.totalDuration; + const framesPerSegment = Math.ceil(HLS_SEGMENT_DURATION * fps); + const fullSegmentDuration = framesPerSegment / fps; + const segmentCount = Math.ceil(packets.outputFrames / framesPerSegment); + const lastSegmentFrames = packets.outputFrames - framesPerSegment * (segmentCount - 1); + const lastSegmentDuration = lastSegmentFrames / fps; + + const lines = [ + '#EXTM3U', + `#EXT-X-VERSION:${HLS_VERSION}`, + `#EXT-X-TARGETDURATION:${HLS_SEGMENT_DURATION}`, + '#EXT-X-MEDIA-SEQUENCE:0', + '#EXT-X-PLAYLIST-TYPE:VOD', + '#EXT-X-MAP:URI="init.mp4"', + ]; + + for (let i = 0; i < segmentCount - 1; i++) { + lines.push(`#EXTINF:${fullSegmentDuration.toFixed(6)},`, `seg_${i}.m4s`); + } + lines.push(`#EXTINF:${lastSegmentDuration.toFixed(6)},`, `seg_${segmentCount - 1}.m4s`, '#EXT-X-ENDLIST', ''); + + return lines.join('\n'); + } + + private getSegmentKey({ sessionId, variantIndex, segmentIndex }: ArgOf<'HlsSegmentResult'>) { + return `${sessionId}:${variantIndex}:${segmentIndex}`; + } + + private getSegmentIndex(session: ApiSession, filename: string) { + if (filename.endsWith('.mp4')) { + return (session.lastRequestedSegment ?? -1) + 1; + } + const segmentIndex = Number.parseInt(HLS_SEGMENT_FILENAME_REGEX.exec(filename)![1]); + session.lastRequestedSegment = segmentIndex; + return segmentIndex; + } + + private trackSession(id: string, variantIndex: number | null = null) { + const session = this.sessions.get(id); + if (!session) { + const newSession = { lastRequestedSegment: null, lastVariantIndex: variantIndex }; + this.sessions.set(id, newSession); + return newSession; + } + + if (session.lastVariantIndex !== null && session.lastVariantIndex !== variantIndex) { + this.pendingSegments.rejectByPrefix(`${id}:${session.lastVariantIndex}:`, 'Variant changed'); + } + session.lastVariantIndex = variantIndex; + return session; + } +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index b733483aa8..3c23e723bc 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -11,6 +11,7 @@ import { DatabaseBackupService } from 'src/services/database-backup.service'; import { DatabaseService } from 'src/services/database.service'; import { DownloadService } from 'src/services/download.service'; import { DuplicateService } from 'src/services/duplicate.service'; +import { HlsService } from 'src/services/hls.service'; import { JobService } from 'src/services/job.service'; import { LibraryService } from 'src/services/library.service'; import { MaintenanceService } from 'src/services/maintenance.service'; @@ -39,6 +40,7 @@ import { SystemMetadataService } from 'src/services/system-metadata.service'; import { TagService } from 'src/services/tag.service'; import { TelemetryService } from 'src/services/telemetry.service'; import { TimelineService } from 'src/services/timeline.service'; +import { TranscodingService } from 'src/services/transcoding.service'; import { TrashService } from 'src/services/trash.service'; import { UserAdminService } from 'src/services/user-admin.service'; import { UserService } from 'src/services/user.service'; @@ -61,6 +63,7 @@ export const services = [ DatabaseService, DownloadService, DuplicateService, + HlsService, JobService, LibraryService, MaintenanceService, @@ -89,6 +92,7 @@ export const services = [ TagService, TelemetryService, TimelineService, + TranscodingService, TrashService, UserAdminService, UserService, diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index d4c425e8bd..48c61c0951 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -41,6 +41,7 @@ describe(QueueService.name, () => { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.HlsSessionCleanup }, { name: JobName.AuditTableCleanup }, { name: JobName.MemoryGenerate }, { name: JobName.UserSyncUsage }, diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index ba6f4c5f3b..d11c9180b2 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -269,6 +269,7 @@ export class QueueService extends BaseService { { name: JobName.PersonCleanup }, { name: JobName.MemoryCleanup }, { name: JobName.SessionCleanup }, + { name: JobName.HlsSessionCleanup }, { name: JobName.AuditTableCleanup }, ); } diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index c03f7bacaa..bd50ab3083 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -75,7 +75,7 @@ export class SearchService extends BaseService { const page = dto.page ?? 1; const size = dto.size || 250; - const userIds = await this.getUserIdsToSearch(auth); + const userIds = await this.getUserIdsToSearch(auth, dto.visibility); const { hasNextPage, items } = await this.searchRepository.searchMetadata( { page, size }, { @@ -103,7 +103,7 @@ export class SearchService extends BaseService { requireElevatedPermission(auth); } - const userIds = await this.getUserIdsToSearch(auth); + const userIds = await this.getUserIdsToSearch(auth, dto.visibility); const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds }); return items.map((item) => mapAsset(item, { auth })); } @@ -113,7 +113,7 @@ export class SearchService extends BaseService { requireElevatedPermission(auth); } - const userIds = await this.getUserIdsToSearch(auth); + const userIds = await this.getUserIdsToSearch(auth, dto.visibility); const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { ...dto, userIds }); return items.map((item) => mapAsset(item, { auth })); } @@ -128,7 +128,7 @@ export class SearchService extends BaseService { throw new BadRequestException('Smart search is not enabled'); } - const userIds = this.getUserIdsToSearch(auth); + const userIds = this.getUserIdsToSearch(auth, dto.visibility); let embedding; if (dto.query) { const key = machineLearning.clip.modelName + dto.query + dto.language; @@ -202,7 +202,11 @@ export class SearchService extends BaseService { } } - private async getUserIdsToSearch(auth: AuthDto): Promise { + private async getUserIdsToSearch(auth: AuthDto, visibility?: AssetVisibility): Promise { + // Locked assets are personal. Never include partner IDs, regardless of A's elevated session. + if (visibility === AssetVisibility.Locked) { + return [auth.user.id]; + } const partnerIds = await getMyPartnerIds({ userId: auth.user.id, repository: this.partnerRepository, diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index d1f08d9b53..e1575a496a 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -148,6 +148,7 @@ describe(ServerService.name, () => { configFile: false, trash: true, email: false, + realtimeTranscoding: false, }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 67bd822d1c..3b66b677a5 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -86,7 +86,7 @@ export class ServerService extends BaseService { } async getFeatures(): Promise { - const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } = + const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications, ffmpeg } = await this.getConfig({ withCache: false }); const { configFile } = this.configRepository.getEnv(); @@ -106,6 +106,7 @@ export class ServerService extends BaseService { passwordLogin: passwordLogin.enabled, configFile: !!configFile, email: notifications.smtp.enabled, + realtimeTranscoding: ffmpeg.realtime.enabled, }; } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index c9a8492b5d..2d0850ac58 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -1,5 +1,6 @@ import { BadRequestException } from '@nestjs/common'; import { defaults, SystemConfig } from 'src/config'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { AudioCodec, Colorspace, @@ -72,6 +73,9 @@ const updatedConfig = Object.freeze({ accel: TranscodeHardwareAcceleration.Disabled, accelDecode: true, tonemap: ToneMapping.Hable, + realtime: { + enabled: false, + }, }, logging: { enabled: true, @@ -184,6 +188,7 @@ const updatedConfig = Object.freeze({ }, newVersionCheck: { enabled: true, + channel: ReleaseChannel.Stable, }, trash: { enabled: true, diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index 4f447f6c3d..ca668b6068 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -204,5 +204,16 @@ describe(TimelineService.name, () => { }), ).rejects.toThrow(BadRequestException); }); + + it('should throw an error if withPartners is true and visibility is locked', async () => { + await expect( + sut.getTimeBucket(authStub.adminWithElevatedPermission, { + timeBucket: 'bucket', + visibility: AssetVisibility.Locked, + withPartners: true, + userId: authStub.adminWithElevatedPermission.user.id, + }), + ).rejects.toThrow(BadRequestException); + }); }); }); diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index 7e9013dc01..b7ecbad9a9 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -71,13 +71,14 @@ export class TimelineService extends BaseService { } if (dto.withPartners) { + const requestedLocked = dto.visibility === AssetVisibility.Locked; const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined; const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; const requestedTrash = dto.isTrashed === true; - if (requestedArchived || requestedFavorite || requestedTrash) { + if (requestedLocked || requestedArchived || requestedFavorite || requestedTrash) { throw new BadRequestException( - 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + 'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets', ); } } diff --git a/server/src/services/transcoding.service.spec.ts b/server/src/services/transcoding.service.spec.ts new file mode 100644 index 0000000000..349cba6a7d --- /dev/null +++ b/server/src/services/transcoding.service.spec.ts @@ -0,0 +1,539 @@ +import { + HLS_BACKPRESSURE_PAUSE_SEGMENTS, + HLS_BACKPRESSURE_RESUME_SEGMENTS, + HLS_CLEANUP_INTERVAL_MS, + HLS_INACTIVITY_TIMEOUT_MS, + HLS_LEASE_DURATION_MS, +} from 'src/constants'; +import { TranscodingService } from 'src/services/transcoding.service'; +import { VIDEO_STREAM_SESSION_PK_CONSTRAINT } from 'src/utils/database'; +import { eiffelTower, train, waterfall } from 'test/fixtures/media.stub'; +import { mockSpawn, newTestService, ServiceMocks } from 'test/utils'; +import { vi } from 'vitest'; + +describe(TranscodingService.name, () => { + let sut: TranscodingService; + let mocks: ServiceMocks; + + const sessionId = 'session-1'; + const assetId = 'asset-1'; + const ownerId = 'user-1'; + + const completeSegment = (index: number) => { + const listener = vi.mocked(mocks.storage.watchDir).mock.lastCall?.[1]; + expect(listener).toBeDefined(); + listener!('rename', `seg_${index}.m4s`); + }; + + const completeSegmentsThrough = (start: number, end: number) => { + for (let i = start; i <= end; i++) { + completeSegment(i); + } + }; + + beforeEach(() => { + ({ sut, mocks } = newTestService(TranscodingService)); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { realtime: { enabled: true } } }); + mocks.videoStream.getForTranscoding.mockResolvedValue(eiffelTower); + }); + + describe('onSessionRequest', () => { + it('creates the session row and emits HlsSessionResult on success', async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + expect(mocks.videoStream.createSession).toHaveBeenCalledWith({ + id: sessionId, + assetId, + expiresAt: expect.any(Date), + }); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionResult', { sessionId }); + }); + + it('treats a primary-key conflict as a no-op for replay tolerance', async () => { + mocks.videoStream.createSession.mockRejectedValue({ constraint_name: VIDEO_STREAM_SESSION_PK_CONSTRAINT }); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + expect(mocks.websocket.serverSend).not.toHaveBeenCalled(); + }); + + it('emits HlsSessionResult with an error on other DB failures', async () => { + mocks.videoStream.createSession.mockRejectedValue(new Error('database is down')); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionResult', { + sessionId, + error: 'Failed to create HLS session', + }); + }); + }); + + describe('onSessionEnd', () => { + it('removes the session, kills the transcode, and deletes the dir + DB row', async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + const process = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValue(process); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + + await sut.onSessionEnd({ sessionId }); + + expect(process.kill).toHaveBeenCalled(); + expect(mocks.storage.unlinkDir).toHaveBeenCalled(); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith(sessionId); + }); + + it('is a no-op when the session is unknown', async () => { + await sut.onSessionEnd({ sessionId: 'never-created' }); + + expect(mocks.videoStream.deleteSession).not.toHaveBeenCalled(); + expect(mocks.storage.unlinkDir).not.toHaveBeenCalled(); + }); + }); + + describe('onHeartbeat', () => { + it('extends the DB lease when remaining time falls below half', async () => { + vi.useFakeTimers(); + try { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + vi.setSystemTime(Date.now() + HLS_LEASE_DURATION_MS / 2 + 1); + + await sut.onHeartbeat({ sessionId }); + + expect(mocks.videoStream.extendSession).toHaveBeenCalledWith(sessionId, expect.any(Date)); + } finally { + vi.useRealTimers(); + } + }); + + it('does not extend the lease while it is still fresh', async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + + await sut.onHeartbeat({ sessionId }); + + expect(mocks.videoStream.extendSession).not.toHaveBeenCalled(); + }); + + it('is a no-op when the session is unknown', async () => { + await sut.onHeartbeat({ sessionId: 'never-created' }); + + expect(mocks.videoStream.extendSession).not.toHaveBeenCalled(); + }); + }); + + describe('onSegmentRequest', () => { + beforeEach(async () => { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + mocks.websocket.serverSend.mockClear(); + }); + + it('spawns FFmpeg on the first request', async () => { + mocks.process.spawn.mockReturnValue(mockSpawn(0, '', '')); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + + expect(mocks.process.spawn).toHaveBeenCalledTimes(1); + expect(mocks.process.spawn).toHaveBeenCalledWith('ffmpeg', expect.any(Array), expect.any(Object)); + }); + + it('kills and respawns when the variant changes', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 1, segmentIndex: 0 }); + + expect(first.kill).toHaveBeenCalled(); + expect(mocks.process.spawn).toHaveBeenCalledTimes(2); + }); + + it('kills and respawns when seeking before the start segment', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 5 }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 2 }); + + expect(first.kill).toHaveBeenCalled(); + expect(mocks.process.spawn).toHaveBeenCalledTimes(2); + }); + + it('kills and respawns when the requested segment is too far ahead', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 5 }); + + expect(first.kill).toHaveBeenCalled(); + expect(mocks.process.spawn).toHaveBeenCalledTimes(2); + }); + + it('does not spawn when the session is unknown', async () => { + await sut.onSegmentRequest({ sessionId: 'never-created', assetId, variantIndex: 0, segmentIndex: 0 }); + + expect(mocks.process.spawn).not.toHaveBeenCalled(); + }); + + it('accepts segments from a restart after the previous ffmpeg exited on its own', async () => { + const first = mockSpawn(0, '', ''); + const second = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(first).mockReturnValueOnce(second); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 10 }); + completeSegment(10); + + const onCalls = vi.mocked(first.on).mock.calls as unknown as [string, (code: number) => void][]; + const exitHandler = onCalls.find(([event]) => event === 'exit')?.[1]; + exitHandler?.(0); + + mocks.websocket.serverSend.mockClear(); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 2 }); + completeSegment(2); + + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSegmentResult', { + sessionId, + variantIndex: 0, + segmentIndex: 2, + }); + }); + }); + + describe('backpressure', () => { + let proc: ReturnType; + + beforeEach(async () => { + proc = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValue(proc); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 0 }); + }); + + it('pauses the transcode once the lead exceeds HLS_BACKPRESSURE_PAUSE_SEGMENTS', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + }); + + it('does not pause when the lead equals the pause threshold', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS); + + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('resumes once the lead drops below HLS_BACKPRESSURE_RESUME_SEGMENTS', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + vi.mocked(proc.kill).mockClear(); + + const requested = HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1 - (HLS_BACKPRESSURE_RESUME_SEGMENTS - 1); + await sut.onHeartbeat({ sessionId, segmentIndex: requested }); + + expect(proc.kill).toHaveBeenCalledWith('SIGCONT'); + }); + + it('stays paused while the lead is in the dead-band', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + vi.mocked(proc.kill).mockClear(); + + const requested = HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1 - HLS_BACKPRESSURE_RESUME_SEGMENTS; + await sut.onHeartbeat({ sessionId, segmentIndex: requested }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('is a no-op when no segment has completed yet', async () => { + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('is a no-op when the heartbeat omits segmentIndex', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + + await sut.onHeartbeat({ sessionId }); + + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it('resumes the paused transcode when the client requests the next in-range segment', async () => { + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + vi.mocked(proc.kill).mockClear(); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex: 1 }); + + expect(proc.kill).toHaveBeenCalledWith('SIGCONT'); + expect(mocks.process.spawn).toHaveBeenCalledTimes(1); + }); + + it('does not re-pause a freshly spawned transcode after a seek-driven restart', async () => { + const newProc = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(newProc); + + completeSegmentsThrough(0, HLS_BACKPRESSURE_PAUSE_SEGMENTS + 1); + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + expect(proc.kill).toHaveBeenCalledWith('SIGSTOP'); + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 1, segmentIndex: 0 }); + vi.mocked(newProc.kill).mockClear(); + + await sut.onHeartbeat({ sessionId, segmentIndex: 0 }); + + expect(newProc.kill).not.toHaveBeenCalled(); + }); + + it('ignores stale segment events from the prior transcode after a backward seek', async () => { + const newProc = mockSpawn(0, '', ''); + mocks.process.spawn.mockReturnValueOnce(newProc); + + const completedAhead = HLS_BACKPRESSURE_PAUSE_SEGMENTS + 5; + completeSegmentsThrough(1, completedAhead); // seg_0 was emitted in beforeEach + + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 1, segmentIndex: 0 }); + + vi.mocked(newProc.kill).mockClear(); + mocks.websocket.serverSend.mockClear(); + completeSegment(completedAhead + 1); + + expect(mocks.websocket.serverSend).not.toHaveBeenCalledWith( + 'HlsSegmentResult', + expect.objectContaining({ segmentIndex: completedAhead + 1 }), + ); + expect(newProc.kill).not.toHaveBeenCalled(); + + completeSegment(0); + expect(mocks.websocket.serverSend).toHaveBeenCalledWith( + 'HlsSegmentResult', + expect.objectContaining({ segmentIndex: 0 }), + ); + }); + }); + + describe('inactivity sweeper', () => { + it('reaps a session whose last activity exceeds the inactivity timeout', async () => { + vi.useFakeTimers(); + try { + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + mocks.websocket.serverSend.mockClear(); + await vi.advanceTimersByTimeAsync(HLS_INACTIVITY_TIMEOUT_MS + HLS_CLEANUP_INTERVAL_MS); + + expect(mocks.websocket.serverSend).toHaveBeenCalledWith('HlsSessionEnd', { sessionId }); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith(sessionId); + } finally { + vi.useRealTimers(); + } + }); + }); + + describe('onShutdown', () => { + it('ends every active session', async () => { + await sut.onSessionRequest({ sessionId: 'session-a', assetId, ownerId }); + await sut.onSessionRequest({ sessionId: 'session-b', assetId, ownerId }); + + await sut.onShutdown(); + + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('session-a'); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('session-b'); + }); + }); + + describe('onHlsSessionCleanup', () => { + it('reaps DB-expired sessions under a database lock', async () => { + mocks.database.withLock.mockImplementation(async (_, fn) => fn()); + mocks.videoStream.getExpiredSessions.mockResolvedValue([ + { id: 'expired-1', ownerId: 'user-a' }, + { id: 'expired-2', ownerId: 'user-b' }, + ]); + + await sut.onHlsSessionCleanup(); + + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('expired-1'); + expect(mocks.videoStream.deleteSession).toHaveBeenCalledWith('expired-2'); + expect(mocks.storage.unlinkDir).toHaveBeenCalledTimes(2); + }); + }); + + describe('FFmpeg full command', () => { + const baseCommand = [ + '-nostdin', + '-nostats', + '-i', + 'eiffel-tower.mp4', + '-map', + '0:0', + '-map_metadata', + '-1', + '-map', + '0:1', + '-g', + '50', + '-keyint_min', + '50', + '-crf', + '23', + '-copyts', + '-r', + '50130000/2012441', + '-avoid_negative_ts', + 'disabled', + '-f', + 'hls', + '-hls_time', + '2', + '-hls_list_size', + '0', + '-hls_segment_type', + 'fmp4', + '-hls_fmp4_init_filename', + 'init.mp4', + '-hls_segment_options', + 'movflags=+frag_discont', + '-hls_flags', + 'temp_file', + '-start_number', + '0', + ]; + + it.each([ + { + variantIndex: 6, + expected: [ + ...baseCommand, + '-c:v', + 'libsvtav1', + '-c:a', + 'aac', + '-preset', + '12', + '-svtav1-params', + 'hierarchical-levels=3:lookahead=0:enable-tf=0:mbr=4000k', + '-hls_segment_filename', + '/data/encoded-video/user-1/se/ss/session-1/6/seg_%d.m4s', + '/data/encoded-video/user-1/se/ss/session-1/6/playlist.m3u8', + ].sort(), + }, + { + variantIndex: 4, + expected: [ + ...baseCommand, + '-c:v', + 'hevc', + '-c:a', + 'aac', + '-tag:v', + 'hvc1', + '-preset', + 'ultrafast', + '-maxrate', + '2500k', + '-bufsize', + '5000k', + '-x265-params', + 'no-scenecut=1:no-open-gop=1', + '-vf', + 'scale=720:-2', + '-hls_segment_filename', + '/data/encoded-video/user-1/se/ss/session-1/4/seg_%d.m4s', + '/data/encoded-video/user-1/se/ss/session-1/4/playlist.m3u8', + ].sort(), + }, + { + variantIndex: 2, + expected: [ + ...baseCommand, + '-c:v', + 'h264', + '-c:a', + 'aac', + '-preset', + 'ultrafast', + '-maxrate', + '2500k', + '-bufsize', + '5000k', + '-sc_threshold:v', + '0', + '-vf', + 'scale=480:-2', + '-hls_segment_filename', + '/data/encoded-video/user-1/se/ss/session-1/2/seg_%d.m4s', + '/data/encoded-video/user-1/se/ss/session-1/2/playlist.m3u8', + ].sort(), + }, + ])('builds the expected FFmpeg command for $codec (variant $variantIndex)', async ({ variantIndex, expected }) => { + mocks.process.spawn.mockReturnValue(mockSpawn(0, '', '')); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex, segmentIndex: 0 }); + + expect(mocks.process.spawn.mock.calls[0][1].toSorted()).toEqual(expected); + }); + }); + + describe('FFmpeg seek per segment', () => { + const eiffelSeeks = [ + 0, 1.987_15, 3.994_372_222_222_222, 6.001_594_444_444_444, 8.008_816_666_666_666, 10.016_038_888_888_888, + 12.023_261_111_111_111, 14.030_483_333_333_333, 16.037_705_555_555_554, 18.044_927_777_777_776, + 20.052_149_999_999_997, 22.059_372_222_222_223, + ]; + const waterfallSeeks = [ + 0, 1.994_642_826_321_467, 4.006_047_357_065_803, 6.017_451_887_810_139_5, 8.028_856_418_554_476, + 10.040_260_949_298_812, + ]; + const trainSeeks = [ + 0, 1.991_666_666_666_666_7, 3.991_666_666_666_666_7, 5.991_666_666_666_666, 7.991_666_666_666_666, + 9.991_666_666_666_667, 11.991_666_666_666_667, 13.991_666_666_666_667, 15.991_666_666_666_667, + 17.991_666_666_666_667, 19.991_666_666_666_667, + ]; + const cases = [ + ...eiffelSeeks.map((expected, segmentIndex) => ({ + name: `${eiffelTower.originalPath} K=${segmentIndex}`, + fixture: eiffelTower, + segmentIndex, + expected, + })), + ...waterfallSeeks.map((expected, segmentIndex) => ({ + name: `${waterfall.originalPath} K=${segmentIndex}`, + fixture: waterfall, + segmentIndex, + expected, + })), + ...trainSeeks.map((expected, segmentIndex) => ({ + name: `${train.originalPath} K=${segmentIndex}`, + fixture: train, + segmentIndex, + expected, + })), + ]; + + it.each(cases)('$name', async ({ fixture, segmentIndex, expected }) => { + mocks.videoStream.getForTranscoding.mockResolvedValue(fixture); + mocks.process.spawn.mockReturnValue(mockSpawn(0, '', '')); + + await sut.onSessionRequest({ sessionId, assetId, ownerId }); + await sut.onSegmentRequest({ sessionId, assetId, variantIndex: 0, segmentIndex }); + + const args = mocks.process.spawn.mock.calls[0][1] as string[]; + if (expected === 0) { + expect(args).toEqual(expect.arrayContaining(['-copyts', '-avoid_negative_ts', 'disabled'])); + expect(args).not.toContain('-ss'); + } else { + expect(args).toEqual( + expect.arrayContaining(['-ss', String(expected), '-copyts', '-avoid_negative_ts', 'disabled']), + ); + } + }); + }); +}); diff --git a/server/src/services/transcoding.service.ts b/server/src/services/transcoding.service.ts new file mode 100644 index 0000000000..69e2529d63 --- /dev/null +++ b/server/src/services/transcoding.service.ts @@ -0,0 +1,387 @@ +import { Injectable } from '@nestjs/common'; +import { ChildProcess } from 'node:child_process'; +import { join } from 'node:path'; +import { + HLS_BACKPRESSURE_PAUSE_SEGMENTS, + HLS_BACKPRESSURE_RESUME_SEGMENTS, + HLS_CLEANUP_INTERVAL_MS, + HLS_INACTIVITY_TIMEOUT_MS, + HLS_LEASE_DURATION_MS, + HLS_SEGMENT_DURATION, + HLS_SEGMENT_FILENAME_REGEX, + HLS_VARIANTS, +} from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { OnEvent, OnJob } from 'src/decorators'; +import { DatabaseLock, ImmichWorker, JobName, QueueName, TranscodeTarget } from 'src/enum'; +import { ArgOf } from 'src/repositories/event.repository'; +import { BaseService } from 'src/services/base.service'; +import { VideoInterfaces } from 'src/types'; +import { isVideoStreamSessionPkConstraint } from 'src/utils/database'; +import { BaseConfig } from 'src/utils/media'; + +type Session = { + assetId: string; + expiresAt: Date; + id: string; + lastActivityTime: Date; + lastClientRequestedSegment: number | null; + lastCompletedSegment: number | null; + ownerId: string; + paused: boolean; + process: ChildProcess | null; + startSegment: number | null; + variantIndex: number | null; +}; + +@Injectable() +export class TranscodingService extends BaseService { + private sessions = new Map(); + private videoInterfaces: VideoInterfaces = { dri: [], mali: false }; + private cleanupInterval: NodeJS.Timeout | null = null; + + @OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Microservices] }) + async onBootstrap() { + const [videoInterfaces] = await Promise.all([this.storageCore.getVideoInterfaces(), this.removeExpiredSessions()]); + this.videoInterfaces = videoInterfaces; + } + + @OnEvent({ name: 'AppShutdown', workers: [ImmichWorker.Microservices] }) + onShutdown() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + return Promise.all([...this.sessions.values()].map(({ id }) => this.onSessionEnd({ sessionId: id }))); + } + + @OnJob({ name: JobName.HlsSessionCleanup, queue: QueueName.BackgroundTask }) + onHlsSessionCleanup() { + return this.removeExpiredSessions(); + } + + @OnEvent({ name: 'HlsSessionRequest', server: true, workers: [ImmichWorker.Microservices] }) + async onSessionRequest({ assetId, sessionId, ownerId }: ArgOf<'HlsSessionRequest'>) { + try { + const expiresAt = new Date(Date.now() + HLS_LEASE_DURATION_MS); + await this.videoStreamRepository.createSession({ id: sessionId, assetId, expiresAt }); + this.sessions.set(sessionId, { + assetId, + expiresAt, + id: sessionId, + lastActivityTime: new Date(), + lastClientRequestedSegment: null, + lastCompletedSegment: null, + ownerId, + paused: false, + process: null, + startSegment: null, + variantIndex: null, + }); + this.cleanupInterval ??= setInterval(() => void this.removeInactiveSessions(), HLS_CLEANUP_INTERVAL_MS); + this.websocketRepository.serverSend('HlsSessionResult', { sessionId }); + } catch (error) { + // If insertion failed due to a PK constraint, another worker has already created a session for this ID. + if (!isVideoStreamSessionPkConstraint(error)) { + this.logger.error(`Failed to create HLS session ${sessionId}: ${error}`); + this.websocketRepository.serverSend('HlsSessionResult', { sessionId, error: 'Failed to create HLS session' }); + } + } + } + + @OnEvent({ name: 'HlsSessionEnd', server: true, workers: [ImmichWorker.Microservices] }) + async onSessionEnd({ sessionId }: ArgOf<'HlsSessionEnd'>) { + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + this.sessions.delete(sessionId); + if (this.cleanupInterval && this.sessions.size === 0) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + this.stopTranscode(session); + await this.removeSessionDir(session); + await this.videoStreamRepository.deleteSession(sessionId); + } + + @OnEvent({ name: 'HlsHeartbeat', server: true, workers: [ImmichWorker.Microservices] }) + async onHeartbeat({ sessionId, segmentIndex }: ArgOf<'HlsHeartbeat'>) { + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + + session.lastActivityTime = new Date(); + + if (segmentIndex !== undefined) { + session.lastClientRequestedSegment = segmentIndex; + this.applyBackpressure(session); + } + + const remaining = session.expiresAt.getTime() - Date.now(); + if (remaining < HLS_LEASE_DURATION_MS / 2) { + session.expiresAt = new Date(Date.now() + HLS_LEASE_DURATION_MS); + await this.videoStreamRepository.extendSession(sessionId, session.expiresAt); + } + } + + @OnEvent({ name: 'HlsSegmentRequest', server: true, workers: [ImmichWorker.Microservices] }) + async onSegmentRequest({ sessionId, variantIndex, segmentIndex }: ArgOf<'HlsSegmentRequest'>) { + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + + session.variantIndex ??= variantIndex; + session.startSegment ??= segmentIndex; + const curSegment = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1; + const needsRestart = + session.variantIndex !== variantIndex || segmentIndex < session.startSegment || segmentIndex > curSegment + 1; + if (needsRestart) { + this.stopTranscode(session); + session.variantIndex = variantIndex; + session.startSegment = segmentIndex; + } else if (session.process) { + this.resumeTranscode(session); + return; + } + + const process = await this.startTranscode(session, variantIndex, segmentIndex); + if (process) { + session.process = process; + } + } + + private applyBackpressure(session: Session) { + if (session.lastCompletedSegment === null || session.lastClientRequestedSegment === null) { + return; + } + const lead = session.lastCompletedSegment - session.lastClientRequestedSegment; + this.logger.debug(`Session ${session.id} lead is ${lead} segments`); + if (!session.paused && lead > HLS_BACKPRESSURE_PAUSE_SEGMENTS) { + this.pauseTranscode(session); + } else if (session.paused && lead < HLS_BACKPRESSURE_RESUME_SEGMENTS) { + this.resumeTranscode(session); + } + } + + private async startTranscode(session: Session, variantIndex: number, startSegment: number) { + const { ffmpeg } = await this.getConfig({ withCache: true }); + + const asset = await this.videoStreamRepository.getForTranscoding(session.assetId); + if (!asset) { + this.logger.error(`Asset ${session.assetId} not found for HLS transcoding`); + return; + } + + if (session.variantIndex !== variantIndex || session.startSegment !== startSegment) { + return; + } + + const variant = HLS_VARIANTS[variantIndex]; + if (!variant) { + this.logger.error(`Variant ${variantIndex} out of range for asset ${session.assetId}`); + await this.failSession(session, `Invalid variant index ${variantIndex}`); + return; + } + + const variantDir = StorageCore.getHlsVariantFolder({ + ownerId: session.ownerId, + sessionId: session.id, + variantIndex, + }); + this.storageRepository.mkdirSync(variantDir); + + // Encoder runs at fps = packetCount × timeBase / totalDuration with + // gop = ceil(SEGMENT_DURATION × fps). To start segment K's content at + // exactly cfr slot K × gop, seek to the midpoint between slots K×gop−1 and + // K×gop. accurate_seek's "discard < target" then keeps the source frame + // that quantizes to slot K×gop and discards the one quantizing to K×gop−1. + const fps = (asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration; + const gop = Math.ceil(HLS_SEGMENT_DURATION * fps); + const seekSeconds = startSegment > 0 ? (startSegment * gop - 0.5) / fps : 0; + + let config; + try { + config = BaseConfig.create( + { + ...ffmpeg, + targetVideoCodec: variant.codec, + targetResolution: String(variant.resolution), + maxBitrate: `${Math.round(variant.bitrate / 1000)}k`, + gopSize: gop, + }, + this.videoInterfaces, + { strictGop: true, lowLatency: true }, + ); + } catch (error: any) { + this.logger.error( + `Failed to create transcode config for variant ${variantIndex} asset ${session.assetId}: ${error?.message ?? error}`, + ); + await this.failSession(session, `Failed to start transcode: ${error?.message ?? 'unknown error'}`); + return; + } + const args = config.getHlsCommand( + { + initFilename: 'init.mp4', + inputPath: asset.originalPath, + packetCount: asset.packets.packetCount, + playlistFilename: join(variantDir, 'playlist.m3u8'), + seekSeconds, + segmentDuration: HLS_SEGMENT_DURATION, + segmentFilename: join(variantDir, 'seg_%d.m4s'), + startSegment, + target: TranscodeTarget.All, + timeBase: asset.videoStream.timeBase, + totalDuration: asset.packets.totalDuration, + }, + asset.videoStream, + asset.audioStream ?? undefined, + ); + this.logger.log( + `Starting HLS transcode for asset ${session.assetId} variant ${variantIndex} with command: ffmpeg ${args.join(' ')}`, + ); + const process = this.processRepository.spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] }); + this.attachProcessHandlers(process, session, variantIndex); + return process; + } + + private failSession(session: Session, error: string) { + this.websocketRepository.serverSend('HlsSessionResult', { sessionId: session.id, error }); + return this.onSessionEnd({ sessionId: session.id }); + } + + private attachProcessHandlers(process: ChildProcess, session: Session, variantIndex: number) { + let stderr = ''; + const variantDir = StorageCore.getHlsVariantFolder({ + ownerId: session.ownerId, + sessionId: session.id, + variantIndex, + }); + + // hlsenc writes each segment as `seg_K.m4s.tmp` then renames to + // `seg_K.m4s`. The rename event fires the moment the renamed file is + // observable — the only signal we need to tell the API worker the + // segment is ready to serve. + const watcher = this.storageRepository.watchDir(variantDir, (eventType, filename) => { + if (eventType !== 'rename' || !filename || session.process !== process) { + return; + } + const match = HLS_SEGMENT_FILENAME_REGEX.exec(filename); + if (!match) { + return; + } + const segmentIndex = Number.parseInt(match[1]); + const expected = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1; + // Ignore stale events from old process after seek + if (expected === null || segmentIndex !== expected) { + return; + } + session.lastCompletedSegment = segmentIndex; + this.websocketRepository.serverSend('HlsSegmentResult', { + sessionId: session.id, + variantIndex, + segmentIndex, + }); + this.applyBackpressure(session); + }); + watcher.on('error', (error) => { + this.logger.error(`watcher error for ${variantDir}: ${error}`); + }); + + process.stderr!.on('data', (chunk: Buffer) => { + if (session.process !== process) { + return; + } + stderr += chunk.toString(); + }); + + process.on('exit', (code) => { + watcher.close(); + if (session.process !== process || session.variantIndex !== variantIndex) { + return; + } + session.paused = false; + session.process = null; + session.lastCompletedSegment = null; + if (code) { + this.logger.error( + `FFmpeg exited with code ${code} for variant ${variantIndex} asset ${session.assetId}\n${stderr}`, + ); + void this.failSession(session, `Transcoding process exited unexpectedly with code ${code}`).catch((error) => + this.logger.error(`Failed to end session ${session.id} after ffmpeg exit: ${error}`), + ); + } + }); + } + + private stopTranscode(session: Session) { + if (!session.process) { + return; + } + // SIGTERM makes it rename .tmp segments to .m4s even if they're still incomplete + session.process.kill('SIGKILL'); + session.process = null; + session.lastCompletedSegment = null; + session.paused = false; + this.logger.debug(`Stopped transcoding for session ${session.id}`); + } + + private pauseTranscode(session: Session) { + if (session.paused || !session.process) { + return; + } + session.process.kill('SIGSTOP'); + session.paused = true; + this.logger.debug(`Paused transcoding for session ${session.id}`); + } + + private resumeTranscode(session: Session) { + if (!session.paused || !session.process) { + return; + } + session.process.kill('SIGCONT'); + session.paused = false; + this.logger.debug(`Resumed transcoding for session ${session.id}`); + } + + private async removeSessionDir(session: { ownerId: string; id: string }) { + const dir = StorageCore.getHlsSessionFolder({ ownerId: session.ownerId, sessionId: session.id }); + try { + await this.storageRepository.unlinkDir(dir, { recursive: true, force: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') { + throw error; + } + this.logger.warn(`Session dir ${dir} does not exist.`); + } + } + + private removeInactiveSessions() { + const cutoff = Date.now() - HLS_INACTIVITY_TIMEOUT_MS; + const inactiveSessions = [...this.sessions.values()].filter((s) => s.lastActivityTime.getTime() < cutoff); + return Promise.all( + inactiveSessions.map(async (session) => { + try { + this.websocketRepository.serverSend('HlsSessionEnd', { sessionId: session.id }); + await this.onSessionEnd({ sessionId: session.id }); + } catch (error) { + this.logger.error(`Failed to sweep inactive HLS session ${session.id}: ${error}`); + } + }), + ); + } + + private removeExpiredSessions() { + return this.databaseRepository.withLock(DatabaseLock.HlsSessionCleanup, async () => { + const expiredSessions = await this.videoStreamRepository.getExpiredSessions(); + await Promise.all( + expiredSessions.map(async (session) => { + await this.removeSessionDir(session); + await this.videoStreamRepository.deleteSession(session.id); + }), + ); + }); + } +} diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 2fbe7292fa..d73edb9850 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -2,6 +2,7 @@ import { DateTime } from 'luxon'; import { SemVer } from 'semver'; import { defaults } from 'src/config'; import { serverVersion } from 'src/constants'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum'; import { VersionService } from 'src/services/version.service'; import { factory } from 'test/small.factory'; @@ -22,6 +23,17 @@ describe(VersionService.name, () => { mocks.cron.update.mockResolvedValue(); }); + beforeAll(() => { + vitest.mock(import('src/constants.js'), async () => ({ + ...(await vitest.importActual('src/constants.js')), + serverVersion: new SemVer('v3.0.0'), + })); + }); + + afterAll(() => { + vitest.unmock(import('src/constants.js')); + }); + it('should work', () => { expect(sut).toBeDefined(); }); @@ -66,9 +78,10 @@ describe(VersionService.name, () => { describe('getVersion', () => { it('should respond the server version', () => { expect(sut.getVersion()).toEqual({ - major: serverVersion.major, - minor: serverVersion.minor, - patch: serverVersion.patch, + major: 3, + minor: 0, + patch: 0, + prerelease: null, }); }); }); @@ -143,24 +156,24 @@ describe(VersionService.name, () => { describe('onConfigUpdate', () => { it('should queue a version check job when newVersionCheck is enabled', async () => { await sut.onConfigUpdate({ - oldConfig: { ...defaults, newVersionCheck: { enabled: false } }, - newConfig: { ...defaults, newVersionCheck: { enabled: true } }, + oldConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } }, + newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} }); }); it('should not queue a version check job when newVersionCheck is disabled', async () => { await sut.onConfigUpdate({ - oldConfig: { ...defaults, newVersionCheck: { enabled: true } }, - newConfig: { ...defaults, newVersionCheck: { enabled: false } }, + oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, + newConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } }, }); expect(mocks.job.queue).not.toHaveBeenCalled(); }); it('should not queue a version check job when newVersionCheck was already enabled', async () => { await sut.onConfigUpdate({ - oldConfig: { ...defaults, newVersionCheck: { enabled: true } }, - newConfig: { ...defaults, newVersionCheck: { enabled: true } }, + oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, + newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } }, }); expect(mocks.job.queue).not.toHaveBeenCalled(); }); @@ -169,21 +182,36 @@ describe(VersionService.name, () => { describe('onWebsocketConnection', () => { it('should send on_server_version client event', async () => { await sut.onWebsocketConnection({ userId: '42' }); - expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); + expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', { + major: 3, + minor: 0, + patch: 0, + prerelease: null, + }); expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(1); }); it('should also send a new release notification', async () => { mocks.systemMetadata.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' }); await sut.onWebsocketConnection({ userId: '42' }); - expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); + expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', { + major: 3, + minor: 0, + patch: 0, + prerelease: null, + }); expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object)); }); it('should not send a release notification when the version check is disabled', async () => { mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } }); await sut.onWebsocketConnection({ userId: '42' }); - expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); + expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', { + major: 3, + minor: 0, + patch: 0, + prerelease: null, + }); expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object)); }); }); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index ce6d6d7a6f..37010db5e7 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -3,19 +3,27 @@ import { DateTime } from 'luxon'; import semver, { SemVer } from 'semver'; import { serverVersion } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; -import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ReleaseEventV1, ReleaseType, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ReleaseChannel } from 'src/dtos/system-config.dto'; import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { VersionCheckMetadata } from 'src/types'; import { handlePromiseError } from 'src/utils/misc'; -const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { +const asNotification = ( + channel: ReleaseChannel, + { checkedAt, releaseVersion }: VersionCheckMetadata, +): ReleaseEventV1 => { return { - isAvailable: semver.gt(releaseVersion, serverVersion), + // can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483 + isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), { + includePrerelease: channel === ReleaseChannel.ReleaseCandidate, + }), checkedAt, serverVersion: ServerVersionResponseDto.fromSemVer(serverVersion), releaseVersion: ServerVersionResponseDto.fromSemVer(new SemVer(releaseVersion)), + type: semver.diff(serverVersion, releaseVersion) as ReleaseType, }; }; @@ -98,14 +106,21 @@ export class VersionService extends BaseService { } } - const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease(); + const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease( + newVersionCheck.channel, + ); const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion }; await this.systemMetadataRepository.set(SystemMetadataKey.VersionCheckState, metadata); - if (semver.gt(releaseVersion, serverVersion)) { + // can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483 + if ( + semver.intersects(`>${serverVersion}`, releaseVersion.toString(), { + includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate, + }) + ) { this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`); - this.websocketRepository.clientBroadcast('on_new_release', asNotification(metadata)); + this.websocketRepository.clientBroadcast('on_new_release', asNotification(newVersionCheck.channel, metadata)); } } catch (error: Error | any) { this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`); @@ -117,7 +132,11 @@ export class VersionService extends BaseService { @OnEvent({ name: 'WebsocketConnect' }) async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) { - this.websocketRepository.clientSend('on_server_version', userId, serverVersion); + this.websocketRepository.clientSend( + 'on_server_version', + userId, + ServerVersionResponseDto.fromSemVer(serverVersion), + ); const { newVersionCheck } = await this.getConfig({ withCache: true }); if (!newVersionCheck.enabled) { @@ -126,7 +145,7 @@ export class VersionService extends BaseService { const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState); if (metadata) { - this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata)); + this.websocketRepository.clientSend('on_new_release', userId, asNotification(newVersionCheck.channel, metadata)); } } } diff --git a/server/src/services/workflow-execution.service.ts b/server/src/services/workflow-execution.service.ts index a1ecf4526d..0a5f025fc1 100644 --- a/server/src/services/workflow-execution.service.ts +++ b/server/src/services/workflow-execution.service.ts @@ -1,10 +1,15 @@ import { CurrentPlugin } from '@extism/extism'; -import { WorkflowChanges, WorkflowEventData, WorkflowEventPayload, WorkflowResponse } from '@immich/plugin-sdk'; +import { + WorkflowChanges, + WorkflowEventData, + WorkflowEventPayload, + WorkflowResponse, + WorkflowTrigger, +} from '@immich/plugin-sdk'; import { HttpException, UnauthorizedException } from '@nestjs/common'; -import _ from 'lodash'; import { join } from 'node:path'; -import { OnEvent, OnJob } from 'src/decorators'; -import { AlbumsAddAssetsDto } from 'src/dtos/album.dto'; +import { DummyValue, OnEvent, OnJob } from 'src/decorators'; +import { AlbumsAddAssetsDto, CreateAlbumDto, GetAlbumsDto } from 'src/dtos/album.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto'; @@ -16,11 +21,11 @@ import { JobName, JobStatus, QueueName, - WorkflowTrigger, WorkflowType, } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { AlbumService } from 'src/services/album.service'; +import { AssetService } from 'src/services/asset.service'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; @@ -32,9 +37,11 @@ const dummy = () => { type ExecuteOptions = { read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData }>; - write: (changes: WorkflowChanges) => Promise; + write: (auth: AuthDto, changes: WorkflowChanges) => Promise; }; +type AssetTrigger = { userId: string; assetId: string; trigger: WorkflowTrigger }; + export class WorkflowExecutionService extends BaseService { private jwtSecret!: string; @@ -59,21 +66,26 @@ export class WorkflowExecutionService extends BaseService { const albumService = BaseService.create(AlbumService, this); - const albumAddAssets = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) => + const searchAlbums = this.wrap<[dto: GetAlbumsDto]>((authDto, args) => albumService.getAll(authDto, ...args)); + const createAlbum = this.wrap<[dto: CreateAlbumDto]>((authDto, args) => albumService.create(authDto, ...args)); + const addAssetsToAlbum = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) => albumService.addAssets(authDto, ...args), ); - const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) => albumService.addAssetsToAlbums(authDto, ...args), ); const functions = { - albumAddAssets, + searchAlbums, + createAlbum, + addAssetsToAlbum, addAssetsToAlbums, }; - const stubs = { - albumAddAssets: dummy, + const stubs: typeof functions = { + searchAlbums: dummy, + createAlbum: dummy, + addAssetsToAlbum: dummy, addAssetsToAlbums: dummy, }; @@ -247,20 +259,36 @@ export class WorkflowExecutionService extends BaseService { } @OnEvent({ name: 'AssetCreate' }) - async onAssetCreate({ asset }: ArgOf<'AssetCreate'>) { - const dto = { ownerId: asset.ownerId, trigger: WorkflowTrigger.AssetCreate }; - const items = await this.workflowRepository.search(dto); + onAssetCreate({ asset: { ownerId: userId, id: assetId } }: ArgOf<'AssetCreate'>) { + return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate }); + } + + @OnEvent({ name: 'AssetMetadataExtracted' }) + onAssetMetadataExtracted({ userId, assetId, source }: ArgOf<'AssetMetadataExtracted'>) { + // prevent loops + // TODO loop detection in job service directly + if (source === 'sidecar-write') { + return; + } + + return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction }); + } + + private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) { + const items = await this.workflowRepository.search({ userId, trigger }); await this.jobRepository.queueAll( items.map((workflow) => ({ - name: JobName.WorkflowAssetCreate, - data: { workflowId: workflow.id, assetId: asset.id }, + name: JobName.WorkflowAssetTrigger, + data: { workflowId: workflow.id, assetId, trigger }, })), ); } - @OnJob({ name: JobName.WorkflowAssetCreate, queue: QueueName.Workflow }) - handleAssetCreate({ workflowId, assetId }: JobOf) { + @OnJob({ name: JobName.WorkflowAssetTrigger, queue: QueueName.Workflow }) + handleAssetTrigger({ workflowId, assetId }: JobOf) { return this.execute(workflowId, (type) => { + const assetService = BaseService.create(AssetService, this); + switch (type) { case WorkflowType.AssetV1: { return { @@ -271,19 +299,35 @@ export class WorkflowExecutionService extends BaseService { authUserId: asset.ownerId, }; }, - write: async (changes) => { - if (changes.asset) { - await this.assetRepository.update({ - id: assetId, - ..._.omitBy( - { - isFavorite: changes.asset?.isFavorite, - visibility: changes.asset?.visibility, - }, - _.isUndefined, - ), - }); + write: async (auth, changes) => { + const asset = changes.asset; + if (!asset) { + return; } + + await assetService.update(auth, assetId, { + isFavorite: asset.isFavorite, + visibility: asset.visibility, + dateTimeOriginal: asset.exifInfo?.dateTimeOriginal ?? undefined, + // TODO allow setting to null + longitude: asset.exifInfo?.longitude ?? undefined, + // TODO allow setting to null + latitude: asset.exifInfo?.latitude ?? undefined, + // TODO allow setting to null + description: asset.exifInfo?.description ?? undefined, + rating: asset.exifInfo?.rating, + + // TODO add to update dto + // make: asset.exifInfo?.make, + // model: asset.exifInfo?.model, + // city: asset.exifInfo?.city, + // state: asset.exifInfo?.state, + // country: asset.exifInfo?.country, + // lensModel: asset.exifInfo?.lensModel, + // fNumber: asset.exifInfo?.fNumber, + // fps: asset.exifInfo?.fps, + // iso: asset.exifInfo?.iso, + }); }, } satisfies ExecuteOptions; } @@ -301,7 +345,19 @@ export class WorkflowExecutionService extends BaseService { } // TODO infer from steps - const type = 'AssetV1' as T; + let type: T | undefined; + for (const targetType of Object.values(WorkflowType)) { + const missing = workflow.steps.some((step) => !step.types.includes(targetType)); + if (!missing) { + type = targetType as unknown as T; + break; + } + } + + if (!type) { + throw new Error('Unable to infer workflow event type from steps'); + } + const handler = getHandler(type); if (!handler) { this.logger.error(`Misconfigured workflow ${workflowId}: no handler for type ${type}`); @@ -337,10 +393,25 @@ export class WorkflowExecutionService extends BaseService { payload, ); if (result?.changes) { - await write(result.changes); + await write( + { + user: { + id: readResult.authUserId, + }, + session: { + id: DummyValue.UUID, + hasElevatedPermission: true, + }, + } as AuthDto, + result.changes, + ); ({ data } = await read(type)); } + if (result?.config) { + await this.workflowRepository.updateStep(step.id, { config: result.config }); + } + const shouldContinue = result?.workflow?.continue ?? true; if (!shouldContinue) { break; diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 0a62a60887..850b4ac086 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -1,4 +1,4 @@ -import { WorkflowStepConfig } from '@immich/plugin-sdk'; +import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk'; import { BadRequestException, Injectable } from '@nestjs/common'; import { AuthDto } from 'src/dtos/auth.dto'; import { @@ -11,7 +11,7 @@ import { WorkflowTriggerResponseDto, WorkflowUpdateDto, } from 'src/dtos/workflow.dto'; -import { Permission, WorkflowTrigger } from 'src/enum'; +import { Permission } from 'src/enum'; import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository'; import { BaseService } from 'src/services/base.service'; import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow'; @@ -23,7 +23,7 @@ export class WorkflowService extends BaseService { } async search(auth: AuthDto, dto: WorkflowSearchDto): Promise { - const workflows = await this.workflowRepository.search({ ...dto, ownerId: auth.user.id }); + const workflows = await this.workflowRepository.search({ ...dto, userId: auth.user.id }); return workflows.map((workflow) => mapWorkflow(workflow)); } diff --git a/server/src/types.ts b/server/src/types.ts index 3b45db0d68..4e5a383cca 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,3 +1,4 @@ +import { WorkflowTrigger } from '@immich/plugin-sdk'; import { ShallowDehydrateObject } from 'kysely'; import { SystemConfig } from 'src/config'; import { VECTOR_EXTENSIONS } from 'src/constants'; @@ -28,8 +29,6 @@ import { SystemMetadataKey, TranscodeTarget, UserMetadataKey, - VideoCodec, - WorkflowTrigger, WorkflowType, } from 'src/enum'; @@ -162,6 +161,25 @@ export interface TranscodeCommand { }; } +export interface VideoTuning { + strictGop: boolean; + lowLatency: boolean; +} + +export interface HlsCommandOptions { + initFilename: string; + inputPath: string; + packetCount: number; + playlistFilename: string; + seekSeconds?: number; + segmentDuration: number; + segmentFilename: string; + startSegment: number; + target: TranscodeTarget; + timeBase: number; + totalDuration: number; +} + export interface BitrateDistribution { max: number; target: number; @@ -177,14 +195,11 @@ export interface ImageBuffer { export interface VideoCodecSWConfig { getCommand( target: TranscodeTarget, - videoStream: VideoStreamInfo, - audioStream?: AudioStreamInfo, + video: VideoStreamInfo, + audio?: AudioStreamInfo, format?: VideoFormat, ): TranscodeCommand; -} - -export interface VideoCodecHWConfig extends VideoCodecSWConfig { - getSupportedCodecs(): Array; + getHlsCommand(options: HlsCommandOptions, video: VideoStreamInfo, audio?: AudioStreamInfo): string[]; } export interface ProbeOptions { @@ -371,6 +386,7 @@ export type JobItem = // Cleanup | { name: JobName.SessionCleanup; data?: IBaseJob } + | { name: JobName.HlsSessionCleanup; data?: IBaseJob } // Tags | { name: JobName.TagCleanup; data?: IBaseJob } @@ -404,7 +420,7 @@ export type JobItem = | { name: JobName.Ocr; data: IEntityJob } // Workflow - | { name: JobName.WorkflowAssetCreate; data: { workflowId: string; assetId: string } } + | { name: JobName.WorkflowAssetTrigger; data: { workflowId: string; assetId: string } } // Editor | { name: JobName.AssetEditThumbnailGeneration; data: IEntityJob }; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index fbf32c0ac2..cb942b5366 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -71,10 +71,13 @@ export const removeUndefinedKeys = (update: T, template: unkno }; export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum'; +export const VIDEO_STREAM_SESSION_PK_CONSTRAINT = 'video_stream_session_pkey'; -export const isAssetChecksumConstraint = (error: unknown) => { - return (error as PostgresError)?.constraint_name === 'UQ_assets_owner_checksum'; -}; +export const isAssetChecksumConstraint = (error: unknown) => + (error as PostgresError)?.constraint_name === ASSET_CHECKSUM_CONSTRAINT; + +export const isVideoStreamSessionPkConstraint = (error: unknown) => + (error as PostgresError)?.constraint_name === VIDEO_STREAM_SESSION_PK_CONSTRAINT; export function withDefaultVisibility(qb: SelectQueryBuilder) { return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.Archive), sql.lit(AssetVisibility.Timeline)]); diff --git a/server/src/utils/event.ts b/server/src/utils/event.ts new file mode 100644 index 0000000000..fd791620de --- /dev/null +++ b/server/src/utils/event.ts @@ -0,0 +1,50 @@ +import { ArgOf, EmitEvent } from 'src/repositories/event.repository'; + +export class PendingEvents extends { error?: string } ? T : never }[EmitEvent]> { + private pending = new Map>[]; timeout: NodeJS.Timeout }>(); + private timeoutMs: number; + + constructor({ timeoutMs }: { timeoutMs: number }) { + this.timeoutMs = timeoutMs; + } + + wait(key: string): Promise> { + const completer = Promise.withResolvers>(); + const existing = this.pending.get(key); + if (existing) { + existing.completers.push(completer); + return completer.promise; + } + + const timeout = setTimeout(() => this.complete(key, { error: 'Request timed out' }), this.timeoutMs); + this.pending.set(key, { completers: [completer], timeout }); + return completer.promise; + } + + complete(key: string, value: ArgOf | { error: string }) { + const pending = this.pending.get(key); + if (!pending) { + return; + } + clearTimeout(pending.timeout); + this.pending.delete(key); + if ('error' in value) { + const error = new Error(value.error); + for (const completer of pending.completers) { + completer.reject(error); + } + } else { + for (const completer of pending.completers) { + completer.resolve(value); + } + } + } + + rejectByPrefix(prefix: string, error: string) { + for (const key of this.pending.keys()) { + if (key.startsWith(prefix)) { + this.complete(key, { error }); + } + } + } +} diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 49e11edab7..b3a617c36e 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -1,4 +1,4 @@ -import { AUDIO_ENCODER } from 'src/constants'; +import { AUDIO_ENCODER, SUPPORTED_HWA_CODECS } from 'src/constants'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { ColorMatrix, @@ -13,38 +13,56 @@ import { import { AudioStreamInfo, BitrateDistribution, + HlsCommandOptions, TranscodeCommand, - VideoCodecHWConfig, VideoCodecSWConfig, VideoFormat, VideoInterfaces, VideoStreamInfo, + VideoTuning, } from 'src/types'; +export const isVideoRotated = (videoStream: VideoStreamInfo): boolean => Math.abs(videoStream.rotation) === 90; + +export const isVideoVertical = (videoStream: VideoStreamInfo): boolean => + videoStream.height > videoStream.width || isVideoRotated(videoStream); + +export const getOutputSize = (videoStream: VideoStreamInfo, targetRes: number) => { + const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width); + let larger = Math.round(targetRes * factor); + if (larger % 2 !== 0) { + larger -= 1; + } + return isVideoVertical(videoStream) ? { width: targetRes, height: larger } : { width: larger, height: targetRes }; +}; + export class BaseConfig implements VideoCodecSWConfig { readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; - protected constructor(protected config: SystemConfigFFmpegDto) {} + protected constructor( + protected config: SystemConfigFFmpegDto, + protected tune: VideoTuning = { strictGop: false, lowLatency: false }, + ) {} - static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces): VideoCodecSWConfig { + static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { if (config.accel === TranscodeHardwareAcceleration.Disabled) { - return this.getSWCodecConfig(config); + return this.getSWCodecConfig(config, tune); } - return this.getHWCodecConfig(config, interfaces); + return this.getHWCodecConfig(config, interfaces, tune); } - private static getSWCodecConfig(config: SystemConfigFFmpegDto) { + private static getSWCodecConfig(config: SystemConfigFFmpegDto, tune?: VideoTuning): VideoCodecSWConfig { switch (config.targetVideoCodec) { case VideoCodec.H264: { - return new H264Config(config); + return new H264Config(config, tune); } case VideoCodec.Hevc: { - return new HEVCConfig(config); + return new HEVCConfig(config, tune); } case VideoCodec.Vp9: { - return new VP9Config(config); + return new VP9Config(config, tune); } case VideoCodec.Av1: { - return new AV1Config(config); + return new AV1Config(config, tune); } default: { throw new Error(`Codec '${config.targetVideoCodec}' is unsupported`); @@ -52,72 +70,122 @@ export class BaseConfig implements VideoCodecSWConfig { } } - private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces) { - let handler: VideoCodecHWConfig; + private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { + if (!SUPPORTED_HWA_CODECS[config.accel].includes(config.targetVideoCodec)) { + throw new Error( + `${config.accel.toUpperCase()} acceleration does not support codec '${config.targetVideoCodec.toUpperCase()}'. Supported codecs: ${SUPPORTED_HWA_CODECS[config.accel]}`, + ); + } + + let handler: VideoCodecSWConfig; switch (config.accel) { case TranscodeHardwareAcceleration.Nvenc: { handler = config.accelDecode - ? new NvencHwDecodeConfig(config, interfaces) - : new NvencSwDecodeConfig(config, interfaces); + ? new NvencHwDecodeConfig(config, interfaces, tune) + : new NvencSwDecodeConfig(config, interfaces, tune); break; } case TranscodeHardwareAcceleration.Qsv: { handler = config.accelDecode - ? new QsvHwDecodeConfig(config, interfaces) - : new QsvSwDecodeConfig(config, interfaces); + ? new QsvHwDecodeConfig(config, interfaces, tune) + : new QsvSwDecodeConfig(config, interfaces, tune); break; } case TranscodeHardwareAcceleration.Vaapi: { handler = config.accelDecode - ? new VaapiHwDecodeConfig(config, interfaces) - : new VaapiSwDecodeConfig(config, interfaces); + ? new VaapiHwDecodeConfig(config, interfaces, tune) + : new VaapiSwDecodeConfig(config, interfaces, tune); break; } case TranscodeHardwareAcceleration.Rkmpp: { handler = config.accelDecode - ? new RkmppHwDecodeConfig(config, interfaces) - : new RkmppSwDecodeConfig(config, interfaces); + ? new RkmppHwDecodeConfig(config, interfaces, tune) + : new RkmppSwDecodeConfig(config, interfaces, tune); break; } default: { throw new Error(`${config.accel.toUpperCase()} acceleration is unsupported`); } } - if (!handler.getSupportedCodecs().includes(config.targetVideoCodec)) { - throw new Error( - `${config.accel.toUpperCase()} acceleration does not support codec '${config.targetVideoCodec.toUpperCase()}'. Supported codecs: ${handler.getSupportedCodecs()}`, - ); - } return handler; } - getCommand( - target: TranscodeTarget, - videoStream: VideoStreamInfo, - audioStream?: AudioStreamInfo, - format?: VideoFormat, - ) { + getCommand(target: TranscodeTarget, video: VideoStreamInfo, audio?: AudioStreamInfo, format?: VideoFormat) { const options = { - inputOptions: this.getBaseInputOptions(videoStream, format), - outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v', 'verbose'], + inputOptions: this.getBaseInputOptions(video, format), + outputOptions: [ + ...this.getBaseOutputOptions(target, video, audio), + ...this.getPresetOptions(), + ...this.getBitrateOptions(), + ...this.getEncoderOptions(), + '-movflags', + 'faststart', + '-fps_mode', + 'passthrough', + '-v', + 'verbose', + ], twoPass: this.eligibleForTwoPass(), - progress: { frameCount: videoStream.frameCount, percentInterval: 5 }, + progress: { frameCount: video.frameCount, percentInterval: 5 }, } as TranscodeCommand; if ([TranscodeTarget.All, TranscodeTarget.Video].includes(target)) { - const filters = this.getFilterOptions(videoStream); + const filters = this.getFilterOptions(video); if (filters.length > 0) { options.outputOptions.push('-vf', filters.join(',')); } } - options.outputOptions.push( + return options; + } + + getHlsCommand(options: HlsCommandOptions, video: VideoStreamInfo, audio?: AudioStreamInfo) { + const args: string[] = this.getBaseInputOptions(video); + if (options.seekSeconds) { + args.push('-ss', String(options.seekSeconds)); + } + args.push( + '-nostdin', + '-nostats', + '-i', + options.inputPath, + ...this.getBaseOutputOptions(options.target, video, audio), ...this.getPresetOptions(), - ...this.getOutputThreadOptions(), ...this.getBitrateOptions(), + ...this.getEncoderOptions(), + '-copyts', + '-r', + `${options.packetCount * options.timeBase}/${options.totalDuration}`, + '-avoid_negative_ts', + 'disabled', + '-f', + 'hls', + '-hls_time', + String(options.segmentDuration), + '-hls_list_size', + '0', + '-hls_segment_type', + 'fmp4', + '-hls_fmp4_init_filename', + options.initFilename, + '-hls_segment_options', + 'movflags=+frag_discont', + '-hls_flags', + 'temp_file', + '-hls_segment_filename', + options.segmentFilename, + '-start_number', + String(options.startSegment), ); - return options; + if ([TranscodeTarget.All, TranscodeTarget.Video].includes(options.target)) { + const filters = this.getFilterOptions(video); + if (filters.length > 0) { + args.push('-vf', filters.join(',')); + } + } + args.push(options.playlistFilename); + return args; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -129,23 +197,7 @@ export class BaseConfig implements VideoCodecSWConfig { const videoCodec = [TranscodeTarget.All, TranscodeTarget.Video].includes(target) ? this.getVideoCodec() : 'copy'; const audioCodec = [TranscodeTarget.All, TranscodeTarget.Audio].includes(target) ? this.getAudioEncoder() : 'copy'; - const options = [ - '-c:v', - videoCodec, - '-c:a', - audioCodec, - // Makes a second pass moving the moov atom to the - // beginning of the file for improved playback speed. - '-movflags', - 'faststart', - '-fps_mode', - 'passthrough', - '-map', - `0:${videoStream.index}`, - '-map_metadata', - '-1', - ]; - + const options = ['-c:v', videoCodec, '-c:a', audioCodec, '-map', `0:${videoStream.index}`, '-map_metadata', '-1']; if (audioStream) { options.push('-map', `0:${audioStream.index}`); } @@ -157,18 +209,22 @@ export class BaseConfig implements VideoCodecSWConfig { } if (this.getGopSize() > 0) { options.push('-g', `${this.getGopSize()}`); + if (this.tune.strictGop) { + options.push('-keyint_min', `${this.getGopSize()}`); + } } - - if ( - this.config.targetVideoCodec === VideoCodec.Hevc && - (videoCodec !== 'copy' || videoStream.codecName === 'hevc') - ) { + const isHvc = (videoCodec === 'copy' ? videoStream.codecName : videoCodec) === VideoCodec.Hevc; + if (isHvc) { options.push('-tag:v', 'hvc1'); } return options; } + getEncoderOptions(): string[] { + return []; + } + getFilterOptions(videoStream: VideoStreamInfo) { const options = []; if (this.shouldScale(videoStream)) { @@ -272,25 +328,7 @@ export class BaseConfig implements VideoCodecSWConfig { getScaling(videoStream: VideoStreamInfo, mult = 2) { const targetResolution = this.getTargetResolution(videoStream); - return this.isVideoVertical(videoStream) ? `${targetResolution}:-${mult}` : `-${mult}:${targetResolution}`; - } - - getSize(videoStream: VideoStreamInfo) { - const smaller = this.getTargetResolution(videoStream); - const factor = Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width); - let larger = Math.round(smaller * factor); - if (larger % 2 !== 0) { - larger -= 1; - } - return this.isVideoVertical(videoStream) ? { width: smaller, height: larger } : { width: larger, height: smaller }; - } - - isVideoRotated(videoStream: VideoStreamInfo) { - return Math.abs(videoStream.rotation) === 90; - } - - isVideoVertical(videoStream: VideoStreamInfo) { - return videoStream.height > videoStream.width || this.isVideoRotated(videoStream); + return isVideoVertical(videoStream) ? `${targetResolution}:-${mult}` : `-${mult}:${targetResolution}`; } isBitrateConstrained() { @@ -353,23 +391,18 @@ export class BaseConfig implements VideoCodecSWConfig { } } -export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { +export class BaseHWConfig extends BaseConfig { protected device: string; - protected interfaces: VideoInterfaces; constructor( protected config: SystemConfigFFmpegDto, - interfaces: VideoInterfaces, + protected interfaces: VideoInterfaces, + tune?: VideoTuning, ) { - super(config); - this.interfaces = interfaces; + super(config, tune); this.device = this.getDevice(interfaces); } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc]; - } - validateDevices(devices: string[]) { if (devices.length === 0) { throw new Error('No /dev/dri devices found. If using Docker, make sure at least one /dev/dri device is mounted'); @@ -474,24 +507,32 @@ export class ThumbnailConfig extends BaseConfig { } export class H264Config extends BaseConfig { - getOutputThreadOptions() { - const options = super.getOutputThreadOptions(); - if (this.config.threads === 1) { - options.push('-x264-params', 'frame-threads=1:pools=none'); + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-sc_threshold:v', '0'); } - - return options; + if (this.config.threads === 1) { + out.push('-x264-params', 'frame-threads=1:pools=none'); + } + return out; } } export class HEVCConfig extends BaseConfig { - getOutputThreadOptions() { - const options = super.getOutputThreadOptions(); - if (this.config.threads === 1) { - options.push('-x265-params', 'frame-threads=1:pools=none'); + getEncoderOptions(): string[] { + const out: string[] = this.getOutputThreadOptions(); + const params: string[] = []; + if (this.tune.strictGop) { + params.push('no-scenecut=1', 'no-open-gop=1'); } - - return options; + if (this.config.threads === 1) { + params.push('frame-threads=1', 'pools=none'); + } + if (params.length > 0) { + out.push('-x265-params', params.join(':')); + } + return out; } } @@ -520,8 +561,8 @@ export class VP9Config extends BaseConfig { return [`-${this.useCQP() ? 'q:v' : 'crf'}`, `${this.config.crf}`, '-b:v', `${bitrates.max}${bitrates.unit}`]; } - getOutputThreadOptions() { - return ['-row-mt', '1', ...super.getOutputThreadOptions()]; + getEncoderOptions(): string[] { + return ['-row-mt', '1', ...this.getOutputThreadOptions()]; } eligibleForTwoPass() { @@ -543,23 +584,22 @@ export class AV1Config extends BaseConfig { } getBitrateOptions() { - const options = ['-crf', `${this.config.crf}`]; - const bitrates = this.getBitrateDistribution(); - const svtparams = []; - if (this.config.threads > 0) { - svtparams.push(`lp=${this.config.threads}`); - } - if (bitrates.max > 0) { - svtparams.push(`mbr=${bitrates.max}${bitrates.unit}`); - } - if (svtparams.length > 0) { - options.push('-svtav1-params', svtparams.join(':')); - } - return options; + return ['-crf', `${this.config.crf}`]; } - getOutputThreadOptions() { - return []; // Already set above with svtav1-params + getEncoderOptions(): string[] { + const params: string[] = []; + if (this.tune.lowLatency) { + params.push('hierarchical-levels=3', 'lookahead=0', 'enable-tf=0'); + } + if (this.config.threads > 0) { + params.push(`lp=${this.config.threads}`); + } + const bitrates = this.getBitrateDistribution(); + if (bitrates.max > 0) { + params.push(`mbr=${bitrates.max}${bitrates.unit}`); + } + return params.length > 0 ? ['-svtav1-params', params.join(':')] : []; } eligibleForTwoPass() { @@ -572,10 +612,6 @@ export class NvencSwDecodeConfig extends BaseHWConfig { return '0'; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Av1]; - } - getBaseInputOptions() { return ['-init_hw_device', `cuda=cuda:${this.device}`, '-filter_hw_device', 'cuda']; } @@ -652,6 +688,14 @@ export class NvencSwDecodeConfig extends BaseHWConfig { return []; } + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-forced-idr', '1'); + } + return out; + } + getRefs() { const bframes = this.getBFrames(); if (bframes > 0 && bframes < 3 && this.config.refs < 3) { @@ -703,8 +747,8 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig { return ['-threads', '1']; } - getOutputThreadOptions() { - return []; + getEncoderOptions(): string[] { + return this.tune.strictGop ? ['-forced-idr', '1'] : []; } } @@ -749,10 +793,6 @@ export class QsvSwDecodeConfig extends BaseHWConfig { return options; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1]; - } - // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md getBFrames() { if (this.config.bframes < 0) { @@ -775,6 +815,14 @@ export class QsvSwDecodeConfig extends BaseHWConfig { getScaling(videoStream: VideoStreamInfo): string { return super.getScaling(videoStream, 1); } + + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-idr_interval', '0'); + } + return out; + } } export class QsvHwDecodeConfig extends QsvSwDecodeConfig { @@ -888,13 +936,17 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { return options; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1]; - } - useCQP() { return this.config.cqMode !== CQMode.Icq || this.config.targetVideoCodec === VideoCodec.Vp9; } + + getEncoderOptions(): string[] { + const out = this.getOutputThreadOptions(); + if (this.tune.strictGop) { + out.push('-idr_interval', '0'); + } + return out; + } } export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { @@ -988,10 +1040,6 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { return ['-rc_mode', 'CQP', '-qp_init', `${this.config.crf}`]; } - getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.Hevc]; - } - getVideoCodec(): string { return `${this.config.targetVideoCodec}_rkmpp`; } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 37fff07fd9..efcb509941 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -15,7 +15,7 @@ import picomatch from 'picomatch'; import parse from 'picomatch/lib/parse'; import { SystemConfig } from 'src/config'; import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants'; -import { extraSyncModels } from 'src/dtos/sync.dto'; +import { extraModels } from 'src/decorators'; import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -289,7 +289,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) const options: SwaggerDocumentOptions = { operationIdFactory: (controllerKey: string, methodKey: string) => methodKey, - extraModels: extraSyncModels, + extraModels, ignoreGlobalPrefix: true, }; diff --git a/server/src/utils/workflow.spec.ts b/server/src/utils/workflow.spec.ts index 86bdd94e5b..5defe92d90 100644 --- a/server/src/utils/workflow.spec.ts +++ b/server/src/utils/workflow.spec.ts @@ -1,4 +1,5 @@ -import { WorkflowTrigger, WorkflowType } from 'src/enum'; +import { WorkflowTrigger } from '@immich/plugin-sdk'; +import { WorkflowType } from 'src/enum'; import { isMethodCompatible } from 'src/utils/workflow'; const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected: boolean }> = [ diff --git a/server/src/utils/workflow.ts b/server/src/utils/workflow.ts index 879fe4c608..5383db818e 100644 --- a/server/src/utils/workflow.ts +++ b/server/src/utils/workflow.ts @@ -1,9 +1,11 @@ -import { WorkflowTrigger, WorkflowType } from 'src/enum'; +import { WorkflowTrigger } from '@immich/plugin-sdk'; +import { WorkflowType } from 'src/enum'; import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository'; export const triggerMap: Record = { [WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1], [WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1], + [WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1], }; export const getWorkflowTriggers = () => diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 85d52f14a1..88a2278898 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -41,6 +41,13 @@ export const authStub = { id: 'token-id', } as AuthSession, }), + adminWithElevatedPermission: Object.freeze({ + user: authUser.admin, + session: { + id: 'token-id-elevated', + hasElevatedPermission: true, + } as AuthSession, + }), adminSharedLink: Object.freeze({ user: authUser.admin, sharedLink: { diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index f034ab873d..7edd1c2dee 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -597,7 +597,7 @@ export const train = { packets: { totalDuration: 12_290, packetCount: 1229, - outputFrames: 1303, + outputFrames: 1304, keyframePts: [ 0, 601, 1201, 1802, 2402, 3003, 3604, 4204, 4805, 5405, 6006, 6607, 7207, 7808, 8408, 9009, 9609, 10_210, 10_811, 11_411, 12_062, 12_703, diff --git a/server/test/medium/specs/services/timeline.service.spec.ts b/server/test/medium/specs/services/timeline.service.spec.ts index cb696366d0..d7013b84be 100644 --- a/server/test/medium/specs/services/timeline.service.spec.ts +++ b/server/test/medium/specs/services/timeline.service.spec.ts @@ -51,13 +51,13 @@ describe(TimelineService.name, () => { const response1 = sut.getTimeBuckets(auth, { withPartners: true, visibility: AssetVisibility.Archive }); await expect(response1).rejects.toBeInstanceOf(BadRequestException); await expect(response1).rejects.toThrow( - 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + 'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets', ); const response2 = sut.getTimeBuckets(auth, { withPartners: true }); await expect(response2).rejects.toBeInstanceOf(BadRequestException); await expect(response2).rejects.toThrow( - 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + 'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets', ); }); @@ -67,13 +67,13 @@ describe(TimelineService.name, () => { const response1 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: false }); await expect(response1).rejects.toBeInstanceOf(BadRequestException); await expect(response1).rejects.toThrow( - 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + 'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets', ); const response2 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: true }); await expect(response2).rejects.toBeInstanceOf(BadRequestException); await expect(response2).rejects.toThrow( - 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + 'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets', ); }); @@ -83,7 +83,7 @@ describe(TimelineService.name, () => { const response = sut.getTimeBuckets(auth, { withPartners: true, isTrashed: true }); await expect(response).rejects.toBeInstanceOf(BadRequestException); await expect(response).rejects.toThrow( - 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + 'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets', ); }); diff --git a/server/test/medium/specs/services/workflow.service.spec.ts b/server/test/medium/specs/services/workflow.service.spec.ts index 07301581cb..b4b52be98c 100644 --- a/server/test/medium/specs/services/workflow.service.spec.ts +++ b/server/test/medium/specs/services/workflow.service.spec.ts @@ -1,5 +1,6 @@ +import { WorkflowTrigger } from '@immich/plugin-sdk'; import { Kysely } from 'kysely'; -import { WorkflowTrigger, WorkflowType } from 'src/enum'; +import { WorkflowType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { PluginRepository } from 'src/repositories/plugin.repository'; diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 99f6c67d5c..2bb9de6af1 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -1,8 +1,8 @@ -import { WorkflowStepConfig } from '@immich/plugin-sdk'; +import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk'; import { Kysely } from 'kysely'; import { readFileSync } from 'node:fs'; import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto'; -import { AssetVisibility, LogLevel, WorkflowTrigger } from 'src/enum'; +import { AssetVisibility, LogLevel } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; @@ -12,6 +12,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { PluginRepository } from 'src/repositories/plugin.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; +import { UserRepository } from 'src/repositories/user.repository'; import { WorkflowRepository } from 'src/repositories/workflow.repository'; import { DB } from 'src/schema'; import { WorkflowExecutionService } from 'src/services/workflow-execution.service'; @@ -33,8 +34,9 @@ class WorkflowTestContext extends MediumTestContext { CryptoRepository, DatabaseRepository, LoggingRepository, - StorageRepository, PluginRepository, + StorageRepository, + UserRepository, WorkflowRepository, ], mock: [ConfigRepository], @@ -137,7 +139,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetArchive' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Archive, @@ -154,7 +156,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetArchive', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Timeline, @@ -173,7 +175,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetLock' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Locked, @@ -190,7 +192,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetLock', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ visibility: AssetVisibility.Timeline, @@ -209,7 +211,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetFavorite' }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true }); }); @@ -224,13 +226,59 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetFavorite', config: { inverse: true } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false }); }); }); describe('assetAddToAlbums', () => { + it('should create an album by name', async () => { + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true }); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [], albumName: 'Screenshots' } }], + }); + + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + + const albums = await ctx.get(AlbumRepository).getAll(user.id); + expect(albums).toHaveLength(1); + + const album = albums[0]!; + expect(album.albumName).toEqual('Screenshots'); + + const updated = await ctx.get(WorkflowRepository).get(workflow.id); + expect(updated?.steps[0].config).toEqual({ albumIds: [album.id], albumName: 'Screenshots' }); + + await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id); + }); + + it('should not use the name when there is an albumId', async () => { + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true }); + const { album } = await ctx.newAlbum({ ownerId: user.id }); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [ + { method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id], albumName: 'Screenshots' } }, + ], + }); + + const albums = await ctx.get(AlbumRepository).getAll(user.id); + expect(albums).toHaveLength(1); + expect(albums[0].albumName).toEqual(album.albumName); + + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + + await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id); + }); + it('should add an asset to an album', async () => { const { user } = await ctx.newUser(); const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true }); @@ -242,7 +290,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id); }); @@ -261,7 +309,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album1.id, album2.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined(); await expect(ctx.get(AlbumRepository).getAssetIds(album1.id, [asset.id])).resolves.toContain(asset.id); await expect(ctx.get(AlbumRepository).getAssetIds(album2.id, [asset.id])).resolves.toContain(asset.id); @@ -279,7 +327,7 @@ describe('core plugin', () => { steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }], }); - await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy(); + await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy(); await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id); }); diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 334d7d0d53..c1fb7ceaa6 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -75,5 +75,6 @@ export const newStorageRepositoryMock = (): Mocked ({ close: vitest.fn(), on: vitest.fn() })), }; }; diff --git a/server/test/utils.ts b/server/test/utils.ts index 75ada7b551..e707971aad 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -181,7 +181,11 @@ export const automock = ( const mocks: Mock[] = []; const instance = new Dependency(...args); - for (const property of Object.getOwnPropertyNames(Dependency.prototype)) { + const propertyNames = new Set([ + ...Object.getOwnPropertyNames(Dependency.prototype), + ...Object.getOwnPropertyNames(instance), + ]); + for (const property of propertyNames) { if (property === 'constructor') { continue; } @@ -346,7 +350,7 @@ export const getMocks = () => { trash: automock(TrashRepository), user: automock(UserRepository, { strict: false }), versionHistory: automock(VersionHistoryRepository), - videoStream: automock(VideoStreamRepository), + videoStream: automock(VideoStreamRepository, { strict: false }), view: automock(ViewRepository), // eslint-disable-next-line no-sparse-arrays websocket: automock(WebsocketRepository, { args: [, loggerMock], strict: false }), @@ -500,6 +504,7 @@ export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: st callback(exitCode); } }), + kill: vitest.fn(), } as unknown as ChildProcessWithoutNullStreams; }); diff --git a/server/tsconfig.json b/server/tsconfig.json index e087544f6b..c3aede3e5b 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -8,9 +8,9 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "target": "es2022", + "target": "es2024", "moduleResolution": "node16", - "lib": ["dom", "es2023"], + "lib": ["dom", "es2024"], "sourceMap": true, "outDir": "./dist", "incremental": true, diff --git a/web/package.json b/web/package.json index 2fb37daaed..36f5cf9647 100644 --- a/web/package.json +++ b/web/package.json @@ -46,6 +46,8 @@ "geojson": "^0.5.0", "handlebars": "^4.7.8", "happy-dom": "^20.0.0", + "hls-video-element": "^1.5.11", + "hls.js": "^1.6.16", "intl-messageformat": "^11.0.0", "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", diff --git a/web/src/lib/components/SchemaConfiguration.svelte b/web/src/lib/components/SchemaConfiguration.svelte index e48f46a402..2cc0b68089 100644 --- a/web/src/lib/components/SchemaConfiguration.svelte +++ b/web/src/lib/components/SchemaConfiguration.svelte @@ -64,7 +64,7 @@ {/each} -{:else if schema.uiHint === 'albumId'} +{:else if schema.uiHint === 'AlbumId'} {:else if schema.enum && schema.array} diff --git a/web/src/lib/components/album-page/AlbumThumbnail.svelte b/web/src/lib/components/album-page/AlbumThumbnail.svelte index 037bb78ab9..e585809dec 100644 --- a/web/src/lib/components/album-page/AlbumThumbnail.svelte +++ b/web/src/lib/components/album-page/AlbumThumbnail.svelte @@ -2,7 +2,7 @@ import AlbumCover from '$lib/components/album-page/AlbumCover.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { getAlbumInfo } from '@immich/sdk'; - import { IconButton, LoadingSpinner } from '@immich/ui'; + import { IconButton, Text, LoadingSpinner } from '@immich/ui'; import { mdiTrashCanOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -46,5 +46,22 @@ /> + {:catch} +
+
+ {$t('unknown')} + {albumId} +
+
+ +
+
{/await} diff --git a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte index 295c5842a0..8f84466295 100644 --- a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte +++ b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte @@ -5,7 +5,7 @@ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte'; import { autoPlayVideo, lang, loopVideo as loopVideoPreference } from '$lib/stores/preferences.store'; - import { getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils'; + import { getAssetHlsSessionUrl, getAssetHlsUrl, getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils'; import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk'; import { Icon, LoadingSpinner } from '@immich/ui'; import { @@ -21,6 +21,9 @@ mdiVolumeMedium, mdiVolumeMute, } from '@mdi/js'; + import Hls, { AbrController, Events, type FragLoadedData, type FragLoadingData, type HlsConfig } from 'hls.js'; + import 'hls-video-element'; + import type HlsVideoElement from 'hls-video-element'; import 'media-chrome/media-control-bar'; import 'media-chrome/media-controller'; import 'media-chrome/media-fullscreen-button'; @@ -28,9 +31,10 @@ import 'media-chrome/media-play-button'; import 'media-chrome/media-playback-rate-button'; import 'media-chrome/media-time-display'; - import 'media-chrome/media-time-range'; + import './immich-time-range'; import 'media-chrome/media-volume-range'; import 'media-chrome/menu/media-playback-rate-menu'; + import 'media-chrome/menu/media-rendition-menu'; import 'media-chrome/menu/media-settings-menu'; import 'media-chrome/menu/media-settings-menu-button'; import 'media-chrome/menu/media-settings-menu-item'; @@ -38,6 +42,8 @@ import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; + import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; + import { mediaCapabilitiesManager } from '$lib/managers/media-capabilities-manager.svelte'; interface Props { asset: AssetResponseDto; @@ -69,14 +75,155 @@ let videoPlayer: HTMLVideoElement | undefined = $state(); let isLoading = $state(true); - let assetFileUrl = $derived( - playOriginalVideo - ? getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Original, cacheKey }) - : getAssetPlaybackUrl({ id: assetId, cacheKey }), - ); + let assetFileUrl = $derived.by(() => { + if (featureFlagsManager.value.realtimeTranscoding) { + return getAssetHlsUrl(assetId); + } + + if (playOriginalVideo) { + return getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Original, cacheKey }); + } + + return getAssetPlaybackUrl({ id: assetId, cacheKey }); + }); const aspectRatio = $derived(asset.width && asset.height ? `${asset.width} / ${asset.height}` : undefined); let showVideo = $state(false); let hasFocused = $state(false); + let activeSession: { assetId: string; id: string } | undefined; + let rebuildCount = 0; + + const MAX_REBUILDS = 1; + const SESSION_ID_REGEX = /\/video\/stream\/([0-9a-f-]{36})\//; + + // hls.js can abandon fetching an in-flight fragment if it thinks it'll take too long, in which case + // it emergency switches to a different variant. This extends the delay even further due to + // cold starting another transcode, so let the fragment finish and have steady ABR decide the next level. + // + // It can also emergency switch between fragments: while a switch's first segment is still loading, + // it can run out of buffer and drop to a lower level for just one segment before continuing at the switched quality. + // This can cause multiple redundant transcoding restarts when it occurs. + // Hold the committed level until its first fragment lands, then resume normal ABR. + class NoAbandonAbrController extends AbrController { + private switchTarget = -1; + + protected override onFragLoading(_event: Events.FRAG_LOADING, data: FragLoadingData) { + if (data.frag.sn === 'initSegment') { + this.switchTarget = data.frag.level; + } + } + + protected override onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) { + if (data.frag.sn !== 'initSegment') { + this.switchTarget = -1; + } + super.onFragLoaded(event, data); + } + + override get nextAutoLevel(): number { + const level = super.nextAutoLevel; + const target = this.hls.levels[this.switchTarget]; + // Hold the committed level, but only while hls.js still considers it healthy. + if (target && level < this.switchTarget && target.loadError === 0 && target.fragmentError === 0) { + return this.switchTarget; + } + return level; + } + + override set nextAutoLevel(level: number) { + super.nextAutoLevel = level; + } + } + + const hlsConfig: Partial = { + abrController: NoAbandonAbrController, + highBufferWatchdogPeriod: 10, + detectStallWithCurrentTimeMs: 10_000, + maxBufferHole: 0.5, + maxBufferLength: 30, + maxMaxBufferLength: 60, + fragLoadPolicy: { + default: { + maxTimeToFirstByteMs: 30_000, + maxLoadTimeMs: 60_000, + timeoutRetry: { maxNumRetry: 5, retryDelayMs: 100, maxRetryDelayMs: 0 }, + errorRetry: { maxNumRetry: 3, retryDelayMs: 1000, maxRetryDelayMs: 8000 }, + }, + }, + useMediaCapabilities: false, + }; + + const releaseSession = () => { + const session = activeSession; + if (!session) { + return; + } + activeSession = undefined; + const url = getAssetHlsSessionUrl(session.assetId, session.id); + void fetch(url, { method: 'DELETE' }).catch(() => console.warn('Failed to release HLS session', session)); + }; + + const isHlsElement = (el: HTMLVideoElement | undefined): el is HlsVideoElement => { + return el?.tagName === 'HLS-VIDEO'; + }; + + const wireHlsListeners = (el: HlsVideoElement, assetId: string, resumeTime?: number) => { + const api = el.api; + if (!api) { + return; + } + + // This is a hack to make the rendition menu use `api.currentLevel` instead of `api.nextLevel`. + // `api.nextLevel` makes the player request the next segment followed by the current segment. + // That backward request causes the server to restart transcoding for no reason. + Object.defineProperty(api, 'nextLevel', { + configurable: true, + get: () => api.currentLevel, + set: (level: number) => { + api.currentLevel = level; + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + api.on(Hls.Events.MANIFEST_PARSED, async () => { + // Defer hls.js's first fragment load until we filter out suboptimal variants + api.stopLoad(); + const id = api.levels[0]?.url[0]?.match(SESSION_ID_REGEX)?.[1]; + if (id) { + activeSession = { assetId, id }; + } + + const keep = await mediaCapabilitiesManager.efficientLevels(api.levels); + for (let i = api.levels.length - 1; i >= 0; i--) { + if (!keep.has(i)) { + api.removeLevel(i); + } + } + + api.startLoad(resumeTime); + }); + + api.on(Hls.Events.FRAG_LOADED, () => (rebuildCount = 0)); + + api.on(Hls.Events.ERROR, (_, data) => { + // 404 on a fragment can mean the server-side session has expired. Refetch + // master for a new session, but give up if it still 404s. + if ( + !data.fatal || + data.details !== Hls.ErrorDetails.FRAG_LOAD_ERROR || + data.response?.code !== 404 || + rebuildCount++ >= MAX_REBUILDS + ) { + console.error('HLS error', JSON.stringify(data)); + return; + } + console.warn('Error loading segment, starting new session'); + activeSession = undefined; + resumeTime = el.currentTime; + el.load(); + // wireHlsListeners must run after el.api is repopulated. + queueMicrotask(() => wireHlsListeners(el, assetId, resumeTime)); + }); + }; onMount(() => { showVideo = true; @@ -84,10 +231,31 @@ $effect(() => { // reactive on `assetFileUrl` changes - if (assetFileUrl) { + if (videoPlayer && assetFileUrl) { hasFocused = false; - videoPlayer?.load(); + rebuildCount = 0; + releaseSession(); + if (isHlsElement(videoPlayer)) { + videoPlayer.config = hlsConfig; + videoPlayer.src = assetFileUrl; + const el = videoPlayer; + queueMicrotask(() => wireHlsListeners(el, assetId)); + } else { + videoPlayer.load(); + } } + return releaseSession; + }); + + const onPagehide = (event: PageTransitionEvent) => { + if (!event.persisted) { + releaseSession(); + } + }; + + $effect(() => { + window.addEventListener('pagehide', onPagehide); + return () => window.removeEventListener('pagehide', onPagehide); }); onDestroy(() => { @@ -144,6 +312,10 @@ videoPlayer?.pause(); } }); + + // The time is only refreshed on HLS fragment decode by default, + // so manually emit events on seek to update it immediately. + const onSeeking = (event: Event) => event.currentTarget?.dispatchEvent(new Event('timeupdate')); {#if showVideo} @@ -172,27 +344,51 @@ style:aspect-ratio={aspectRatio} defaultduration={asset.duration! / 1000} > - + {#if featureFlagsManager.value.realtimeTranscoding} + handleCanPlay(e.currentTarget as HTMLVideoElement)} + onended={onVideoEnded} + onseeking={onSeeking} + onplaying={(e: Event) => { + if (!hasFocused) { + (e.currentTarget as HTMLElement).focus(); + hasFocused = true; + } + }} + onclose={onClose} + poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey })} + > + {:else} + + {/if} {#if extendedControls} {/if} @@ -238,7 +444,7 @@ {/if} - + @@ -248,7 +454,7 @@ {/if} - {#if assetViewerManager.isFaceEditMode} + {#if assetViewerManager.isFaceEditMode && videoPlayer} {/if} {/if} @@ -291,12 +497,12 @@ font-variant-numeric: tabular-nums; } - media-time-range, + immich-time-range, media-volume-range { --media-control-hover-background: none; } - media-time-range:hover, + immich-time-range:hover, media-volume-range:hover { --media-range-thumb-opacity: 1; } diff --git a/web/src/lib/components/asset-viewer/immich-time-range.ts b/web/src/lib/components/asset-viewer/immich-time-range.ts new file mode 100644 index 0000000000..a3de131e73 --- /dev/null +++ b/web/src/lib/components/asset-viewer/immich-time-range.ts @@ -0,0 +1,54 @@ +import { MediaUIEvents } from 'media-chrome/constants'; +import MediaTimeRange from 'media-chrome/media-time-range'; + +const COMMIT_DELAY_MS = 750; + +/** Custom MediaTimeRange that only seeks after pointer release to avoid hammering the server. + * Keyboard input uses timed debouncing instead since there's no release event. */ +class ImmichTimeRange extends MediaTimeRange { + private seeking = false; + private pending: number | undefined; + private idleTimer: ReturnType | undefined; + + override connectedCallback() { + super.connectedCallback(); + this.addEventListener('pointerdown', this.hold); + this.addEventListener('keydown', this.hold); + this.addEventListener('pointerup', this.release); + this.addEventListener('pointercancel', this.release); + this.addEventListener(MediaUIEvents.MEDIA_SEEK_REQUEST, this.intercept, { capture: true }); + } + + private hold(event: Event) { + if (event instanceof KeyboardEvent) { + if (!this.keysUsed.includes(event.key)) { + return; + } + clearTimeout(this.idleTimer); + this.idleTimer = setTimeout(this.release, COMMIT_DELAY_MS); + } + this.seeking = true; + } + + private intercept(event: Event) { + if (!this.seeking) { + return; // not mid-scrub, or this is the request we replay in release() + } + this.pending = (event as CustomEvent).detail; + event.stopImmediatePropagation(); + } + + private release() { + clearTimeout(this.idleTimer); + this.seeking = false; + if (this.pending !== undefined) { + const detail = this.pending; + this.pending = undefined; + this.dispatchEvent(new CustomEvent(MediaUIEvents.MEDIA_SEEK_REQUEST, { bubbles: true, composed: true, detail })); + } + } +} + +if (!globalThis.customElements.get('immich-time-range')) { + globalThis.customElements.define('immich-time-range', ImmichTimeRange); +} diff --git a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte index a087aaf809..aef52d0cfd 100644 --- a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte @@ -205,11 +205,7 @@
{ + const getReleaseInfo = (release?: ReleaseEventV1) => { if (!release || !release?.isAvailable || !authManager.user.isAdmin) { return; } diff --git a/web/src/lib/components/timeline/AssetLayout.svelte b/web/src/lib/components/timeline/AssetLayout.svelte index 88f03ef46a..59b8499d79 100644 --- a/web/src/lib/components/timeline/AssetLayout.svelte +++ b/web/src/lib/components/timeline/AssetLayout.svelte @@ -30,14 +30,11 @@ const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150); const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100); - - const firstInOrNearViewport = $derived(viewerAssets.findIndex((a) => a.isInOrNearViewport)); - const lastInOrNearViewport = $derived(viewerAssets.findLastIndex((a) => a.isInOrNearViewport));
- {#each viewerAssets.slice(firstInOrNearViewport, lastInOrNearViewport + 1) as viewerAsset (viewerAsset.id)} + {#each viewerAssets as viewerAsset (viewerAsset.id)} {@const position = viewerAsset.position!} {@const asset = viewerAsset.asset!} diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte index 3f4b9b0661..7cbfd67ddf 100644 --- a/web/src/lib/components/timeline/Month.svelte +++ b/web/src/lib/components/timeline/Month.svelte @@ -100,7 +100,7 @@ >(); + + init() { + for (const level of [ + { videoCodec: 'av01.0.04M.08', width: 854, height: 480, bitrate: 1_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.1.6.L90.B0', width: 854, height: 480, bitrate: 1_200_000, frameRate: 60 }, + { videoCodec: 'av01.0.08M.08', width: 1280, height: 720, bitrate: 2_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.1.6.L93.B0', width: 1280, height: 720, bitrate: 2_500_000, frameRate: 60 }, + { videoCodec: 'av01.0.09M.08', width: 1920, height: 1080, bitrate: 4_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.1.6.L120.B0', width: 1920, height: 1080, bitrate: 4_500_000, frameRate: 60 }, + { videoCodec: 'av01.0.12M.08', width: 2560, height: 1440, bitrate: 7_000_000, frameRate: 60 }, + { videoCodec: 'hvc1.2.4.L150.B0', width: 2560, height: 1440, bitrate: 8_000_000, frameRate: 60 }, + ]) { + this.cache.set(this.cacheKey(level), this.queryDecodingInfo(level)); + } + + for (const level of [ + { videoCodec: 'avc1.64001e', width: 854, height: 480, bitrate: 2_500_000, frameRate: 60 }, + { videoCodec: 'avc1.64001f', width: 1280, height: 720, bitrate: 5_000_000, frameRate: 60 }, + { videoCodec: 'avc1.640028', width: 1920, height: 1080, bitrate: 8_000_000, frameRate: 60 }, + { videoCodec: 'avc1.640032', width: 2560, height: 1440, bitrate: 16_000_000, frameRate: 60 }, + ]) { + this.cache.set(this.cacheKey(level), Promise.resolve(DEFAULT_DECODING_INFO)); + } + } + + async efficientLevels(levels: Level[]) { + const decodingInfo = await Promise.all(levels.map((level) => this.decodingInfo(level))); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const lowestBitrateByHeight = new Map(); + for (let i = 0; i < levels.length; i++) { + if (!decodingInfo[i].powerEfficient) { + continue; + } + + const { bitrate, height } = levels[i]; + const cur = lowestBitrateByHeight.get(height); + if (cur === undefined || bitrate < levels[cur].bitrate) { + lowestBitrateByHeight.set(height, i); + } + } + + return new Set(lowestBitrateByHeight.values()); + } + + decodingInfo(level: Level) { + const key = this.cacheKey(level); + const existing = this.cache.get(key); + if (existing) { + return existing; + } + const promise = this.queryDecodingInfo(level); + this.cache.set(key, promise); + return promise; + } + + private async queryDecodingInfo(level: Level) { + try { + return await navigator.mediaCapabilities.decodingInfo({ + type: 'media-source', + video: { + contentType: `video/mp4; codecs="${level.videoCodec}"`, + width: level.width, + height: level.height, + bitrate: level.bitrate, + framerate: level.frameRate, + }, + }); + } catch { + return DEFAULT_DECODING_INFO; + } + } + + private cacheKey({ videoCodec, width, height, frameRate }: Level) { + const resolution = Math.min(width, height); + const fpsBucket = Math.trunc(frameRate / 61) * 60; + return `${videoCodec}|${resolution}|${fpsBucket}`; + } +} + +export const mediaCapabilitiesManager = new MediaCapabilitiesManager(); +mediaCapabilitiesManager.init(); diff --git a/web/src/lib/managers/release-manager.svelte.ts b/web/src/lib/managers/release-manager.svelte.ts index 15baa6de8f..fed57dd3d8 100644 --- a/web/src/lib/managers/release-manager.svelte.ts +++ b/web/src/lib/managers/release-manager.svelte.ts @@ -1,8 +1,8 @@ +import type { ReleaseEventV1 } from '@immich/sdk'; import { eventManager } from '$lib/managers/event-manager.svelte'; -import { type ReleaseEvent } from '$lib/types'; class ReleaseManager { - value = $state(); + value = $state(); constructor() { eventManager.on({ diff --git a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts index bb3ae72c81..3175fd3c3e 100644 --- a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts @@ -53,17 +53,3 @@ export function updateTimelineMonthViewportProximity(timelineManager: TimelineMa timelineManager.clearDeferredLayout(month); } } - -export function calculateViewerAssetViewportProximity( - timelineManager: TimelineManager, - positionTop: number, - positionHeight: number, -) { - const headerHeight = timelineManager.headerHeight; - return calculateViewportProximity( - positionTop, - positionTop + positionHeight, - timelineManager.visibleWindow.top - headerHeight, - timelineManager.visibleWindow.bottom + headerHeight, - ); -} diff --git a/web/src/lib/managers/timeline-manager/timeline-day.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-day.svelte.ts index c3d53be67a..2515acc385 100644 --- a/web/src/lib/managers/timeline-manager/timeline-day.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-day.svelte.ts @@ -1,12 +1,31 @@ import { AssetOrder, AssetOrderBy } from '@immich/sdk'; import { SvelteSet } from 'svelte/reactivity'; -import type { CommonLayoutOptions } from '$lib/utils/layout-utils'; +import type { CommonLayoutOptions, CommonPosition } from '$lib/utils/layout-utils'; import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils'; import { getOrderingDate, plainDateTimeCompare } from '$lib/utils/timeline-util'; +import { TUNABLES } from '$lib/utils/tunables'; import type { TimelineMonth } from './timeline-month.svelte'; import type { Direction, MoveAsset, TimelineAsset } from './types'; import { ViewerAsset } from './viewer-asset.svelte'; +const { + TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM }, +} = TUNABLES; + +function lowerBound(assets: ViewerAsset[], target: number, key: (pos: CommonPosition) => number): number { + let lo = 0; + let hi = assets.length; + while (lo < hi) { + const mid = Math.floor((lo + hi) / 2); + if (key(assets[mid].position!) < target) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; +} + export class TimelineDay { readonly timelineMonth: TimelineMonth; readonly index: number; @@ -18,12 +37,15 @@ export class TimelineDay { height = $state(0); width = $state(0); + // Assets in or near the viewport; active assets should be added to the DOM. + activeViewerAssets: ViewerAsset[] = $state([]); + isInOrNearViewport = $state(false); + #top: number = $state(0); #start: number = $state(0); #row = $state(0); #col = $state(0); #deferredLayout = false; - #lastInOrNearViewport = -1; constructor(timelineMonth: TimelineMonth, index: number, day: number, groupTitle: string, orderBy: AssetOrderBy) { this.index = index; @@ -149,18 +171,32 @@ export class TimelineDay { for (let i = 0; i < this.viewerAssets.length; i++) { this.viewerAssets[i].position = geometry.getPosition(i); } + this.updateAssetBoundaries(); + } + + updateAssetBoundaries() { + const manager = this.timelineMonth.timelineManager; + const visibleWindow = manager.visibleWindow; + if (this.viewerAssets.length === 0 || !this.viewerAssets[0].position) { + this.activeViewerAssets = []; + this.isInOrNearViewport = false; + return; + } + + const dayOffset = this.absoluteTimelineDayTop; + const headerHeight = manager.headerHeight; + const expandedTop = visibleWindow.top - headerHeight - INTERSECTION_EXPAND_TOP - dayOffset; + const expandedBottom = visibleWindow.bottom + headerHeight + INTERSECTION_EXPAND_BOTTOM - dayOffset; + + const first = lowerBound(this.viewerAssets, expandedTop, (p) => p.top + p.height); + const last = lowerBound(this.viewerAssets, expandedBottom, (p) => p.top) - 1; + + const hasActive = last >= first && first < this.viewerAssets.length; + this.activeViewerAssets = hasActive ? this.viewerAssets.slice(first, last + 1) : []; + this.isInOrNearViewport = hasActive; } get absoluteTimelineDayTop() { return this.timelineMonth.top + this.#top; } - - get isInOrNearViewport() { - if (this.#lastInOrNearViewport !== -1 && this.viewerAssets[this.#lastInOrNearViewport].isInOrNearViewport) { - return true; - } - - this.#lastInOrNearViewport = this.viewerAssets.findIndex((viewAsset) => viewAsset.isInOrNearViewport); - return this.#lastInOrNearViewport !== -1; - } } diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 32e6d90d1c..5ac2deaa54 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -214,6 +214,11 @@ export class TimelineManager extends VirtualScrollManager { for (const month of this.months) { updateTimelineMonthViewportProximity(this, month); + if (month.isInOrNearViewport && month.isLoaded) { + for (const day of month.timelineDays) { + day.updateAssetBoundaries(); + } + } } const month = this.months.find((month) => month.isInViewport); diff --git a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts index b6706e1ea1..54a0edc784 100644 --- a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts @@ -254,7 +254,7 @@ export class TimelineMonth { addContext.newTimelineDays.add(timelineDay); } - const viewerAsset = new ViewerAsset(timelineDay, timelineAsset); + const viewerAsset = new ViewerAsset(timelineAsset); timelineDay.viewerAssets.push(viewerAsset); addContext.changedTimelineDays.add(timelineDay); } diff --git a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts index 0179dd9e74..3a4ed4545c 100644 --- a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts +++ b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts @@ -1,36 +1,12 @@ import type { CommonPosition } from '$lib/utils/layout-utils'; -import { - ViewportProximity, - calculateViewerAssetViewportProximity, - isInOrNearViewport, -} from './internal/intersection-support.svelte'; -import type { TimelineDay } from './timeline-day.svelte'; import type { TimelineAsset } from './types'; export class ViewerAsset { - readonly #group: TimelineDay; - - #viewportProximity = $derived.by(() => { - if (!this.position) { - return ViewportProximity.FarFromViewport; - } - - const store = this.#group.timelineMonth.timelineManager; - const positionTop = this.#group.absoluteTimelineDayTop + this.position.top; - - return calculateViewerAssetViewportProximity(store, positionTop, this.position.height); - }); - - get isInOrNearViewport() { - return isInOrNearViewport(this.#viewportProximity); - } - position: CommonPosition | undefined = $state.raw(); asset: TimelineAsset = $state() as TimelineAsset; id: string = $derived(this.asset.id); - constructor(group: TimelineDay, asset: TimelineAsset) { - this.#group = group; + constructor(asset: TimelineAsset) { this.asset = asset; } } diff --git a/web/src/lib/modals/PluginMethodPicker.svelte b/web/src/lib/modals/PluginMethodPicker.svelte index bbfa2ca83f..230f5a2257 100644 --- a/web/src/lib/modals/PluginMethodPicker.svelte +++ b/web/src/lib/modals/PluginMethodPicker.svelte @@ -24,7 +24,7 @@
{method.title} - {#if method.uiHints.includes('filter')} + {#if method.uiHints.includes('Filter')} {$t('plugin_method_filter_type')} diff --git a/web/src/lib/modals/WorkflowEditTrigger.svelte b/web/src/lib/modals/WorkflowEditTrigger.svelte deleted file mode 100644 index d72bff3c6d..0000000000 --- a/web/src/lib/modals/WorkflowEditTrigger.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - -
- {#each pluginManager.triggers as item (item.trigger)} - (selected = item)}> -
- {getTriggerName($t, item.trigger)} - {getTriggerDescription($t, item.trigger)} -
-
- {/each} -
-
diff --git a/web/src/lib/modals/WorkflowTemplatePicker.svelte b/web/src/lib/modals/WorkflowTemplatePickerModal.svelte similarity index 87% rename from web/src/lib/modals/WorkflowTemplatePicker.svelte rename to web/src/lib/modals/WorkflowTemplatePickerModal.svelte index 05306671fc..0f3aad0ee6 100644 --- a/web/src/lib/modals/WorkflowTemplatePicker.svelte +++ b/web/src/lib/modals/WorkflowTemplatePickerModal.svelte @@ -2,7 +2,7 @@ import { pluginManager } from '$lib/managers/plugin-manager.svelte'; import { handleCreateWorkflow } from '$lib/services/workflow.service'; import { type PluginTemplateResponseDto } from '@immich/sdk'; - import { FormModal, Icon, ListButton, Text } from '@immich/ui'; + import { Badge, FormModal, Icon, ListButton, Text } from '@immich/ui'; import { mdiFlashOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -59,6 +59,11 @@ {template.title} {template.description}
+ {#if template.uiHints.includes('SmartAlbum')} +
+ {$t('smart_album')} +
+ {/if}
{/each} diff --git a/web/src/lib/route.ts b/web/src/lib/route.ts index 734be99402..1846f29796 100644 --- a/web/src/lib/route.ts +++ b/web/src/lib/route.ts @@ -152,4 +152,13 @@ export const Route = { // queues queues: () => '/admin/queues', viewQueue: ({ name }: { name: QueueName }) => `/admin/queues/${asQueueSlug(name)}`, + + // continue helper for ensuring same-origin URLs + continue: (url: string | null, fallback: string) => { + if (!url || !url.startsWith('/') || url.startsWith('//')) { + return fallback; + } + + return url; + }, }; diff --git a/web/src/lib/services/workflow.service.ts b/web/src/lib/services/workflow.service.ts index 79358b9ee4..c81b1b2706 100644 --- a/web/src/lib/services/workflow.service.ts +++ b/web/src/lib/services/workflow.service.ts @@ -26,7 +26,7 @@ import type { MessageFormatter } from 'svelte-i18n'; import { goto } from '$app/navigation'; import { eventManager } from '$lib/managers/event-manager.svelte'; import WorkflowDuplicateModal from '$lib/modals/WorkflowDuplicateModal.svelte'; -import WorkflowTemplatePicker from '$lib/modals/WorkflowTemplatePicker.svelte'; +import WorkflowTemplatePickerModal from '$lib/modals/WorkflowTemplatePickerModal.svelte'; import { Route } from '$lib/route'; import { copyToClipboard, downloadJson } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; @@ -50,7 +50,7 @@ export const getWorkflowsActions = ($t: MessageFormatter) => { const UseTemplate: ActionItem = { title: $t('browse_templates'), icon: mdiFileDocumentMultipleOutline, - onAction: () => modalManager.show(WorkflowTemplatePicker, {}), + onAction: () => modalManager.show(WorkflowTemplatePickerModal, {}), }; return { Create, UseTemplate }; diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 5765f85a16..fc33812973 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -3,6 +3,7 @@ import { type AssetResponseDto, type MaintenanceStatusResponseDto, type NotificationDto, + type ReleaseEventV1, type ServerVersionResponseDto, type SyncAssetEditV1, type SyncAssetV2, @@ -15,7 +16,6 @@ import { eventManager } from '$lib/managers/event-manager.svelte'; import { Route } from '$lib/route'; import { maintenanceStore } from '$lib/stores/maintenance.store'; import { notificationManager } from '$lib/stores/notification-manager.svelte'; -import type { ReleaseEvent } from '$lib/types'; import { createEventEmitter } from '$lib/utils/eventemitter'; interface AppRestartEvent { @@ -34,7 +34,7 @@ export interface Events { on_person_thumbnail: (personId: string) => void; on_server_version: (serverVersion: ServerVersionResponseDto) => void; on_config_update: () => void; - on_new_release: (event: ReleaseEvent) => void; + on_new_release: (event: ReleaseEventV1) => void; on_session_delete: (sessionId: string) => void; on_notification: (notification: NotificationDto) => void; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b0e1466da1..41d98df097 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1,4 +1,4 @@ -import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk'; +import type { QueueResponseDto } from '@immich/sdk'; import type { ActionItem } from '@immich/ui'; import type { DateTime } from 'luxon'; import type { SvelteSet } from 'svelte/reactivity'; @@ -7,14 +7,6 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; export type LatLng = { lng: number; lat: number }; -export interface ReleaseEvent { - isAvailable: boolean; - /** ISO8601 */ - checkedAt: string; - serverVersion: ServerVersionResponseDto; - releaseVersion: ServerVersionResponseDto; -} - export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] }; export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } }; @@ -104,7 +96,7 @@ export type JSONSchemaProperty = { array?: boolean; properties?: Record; required?: string[]; - uiHint?: 'albumId' | 'assetId' | 'personId'; + uiHint?: 'AlbumId' | 'AssetId' | 'PersonId'; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/web/src/lib/utils.spec.ts b/web/src/lib/utils.spec.ts index 9ecc7f548e..992cd803e7 100644 --- a/web/src/lib/utils.spec.ts +++ b/web/src/lib/utils.spec.ts @@ -1,5 +1,5 @@ import { AssetTypeEnum } from '@immich/sdk'; -import { getAssetUrl, getReleaseType } from '$lib/utils'; +import { getAssetUrl, semverToName } from '$lib/utils'; import { assetFactory } from '@test-data/factories/asset-factory'; import { sharedLinkFactory } from '@test-data/factories/shared-link-factory'; @@ -161,26 +161,13 @@ describe('utils', () => { expect(url).toContain(asset.id); }); }); - - describe(getReleaseType.name, () => { - it('should return "major" for major version changes', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 2, minor: 0, patch: 0 })).toBe('major'); - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 3, minor: 2, patch: 1 })).toBe('major'); + describe('semverToName', () => { + it('should not append release candidate tag if prelease is not set', () => { + expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: null })).toEqual('v3.0.0'); }); - it('should return "minor" for minor version changes', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 1, patch: 0 })).toBe('minor'); - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 2, patch: 1 })).toBe('minor'); - }); - - it('should return "patch" for patch version changes', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 1 })).toBe('patch'); - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 5 })).toBe('patch'); - }); - - it('should return "none" for matching versions', () => { - expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 })).toBe('none'); - expect(getReleaseType({ major: 1, minor: 2, patch: 3 }, { major: 1, minor: 2, patch: 3 })).toBe('none'); + it('should append release candidate if set', () => { + expect(semverToName({ major: 3, minor: 0, patch: 0, prerelease: 0 })).toEqual('v3.0.0-rc.0'); }); }); }); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 397e32e136..4ff3564ffa 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -244,6 +244,14 @@ export const getAssetPlaybackUrl = (options: AssetUrlOptions) => { return createUrl(getAssetPlaybackPath(id), { ...authManager.params, c }); }; +export const getAssetHlsUrl = (id: string) => { + return createUrl(`/assets/${id}/video/stream/main.m3u8`, authManager.params); +}; + +export const getAssetHlsSessionUrl = (id: string, sessionId: string) => { + return createUrl(`/assets/${id}/video/stream/${sessionId}`, authManager.params); +}; + export const getProfileImageUrl = (user: UserResponseDto) => createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt }); @@ -411,26 +419,8 @@ export function createDateFormatter(localeCode: string | undefined): DateFormatt }; } -export const getReleaseType = ( - current: ServerVersionResponseDto, - newVersion: ServerVersionResponseDto, -): 'major' | 'minor' | 'patch' | 'none' => { - if (current.major !== newVersion.major) { - return 'major'; - } - - if (current.minor !== newVersion.minor) { - return 'minor'; - } - - if (current.patch !== newVersion.patch) { - return 'patch'; - } - - return 'none'; -}; - -export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`; +export const semverToName = ({ major, minor, patch, prerelease }: ServerVersionResponseDto) => + `v${major}.${minor}.${patch}${prerelease === null ? '' : `-rc.${prerelease}`}`; export const withoutIcons = (actions: ActionItem[]): ActionItem[] => actions.map((action) => ({ ...action, icon: undefined })); diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte index 49e1a456ee..6110572e5b 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepCard.svelte @@ -1,5 +1,25 @@ + + @@ -132,8 +153,8 @@
(isDropTarget = false)} ondrop={(event) => handleDrop(index, event)} @@ -207,15 +228,28 @@ {#if configEntries.length > 0}
- {#each configEntries as [key, value] (key)} + {#snippet badge(key: string, content: string)} - {key}{formatConfigValue(value)} + {key}{content} + {/snippet} + {#each configEntries as [key, value] (key)} + {#if getUiHint(key) === 'AlbumId'} + {#each toIds(value) as albumId (albumId)} + {#await getAlbumName(albumId)} + {@render badge($t('album'), '…')} + {:then albumName} + {@render badge($t('album'), `"${truncate(albumName)}"`)} + {/await} + {/each} + {:else} + {@render badge(key, formatConfigValue(value))} + {/if} {/each}
diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepDragImage.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepDragImage.svelte index b538792e0e..4c23bbffa0 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepDragImage.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowStepDragImage.svelte @@ -13,6 +13,7 @@ + + +
+ +
+
diff --git a/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte b/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte index 71d8424e2a..653448e062 100644 --- a/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte +++ b/web/src/routes/admin/system-settings/NewVersionCheckSettings.svelte @@ -5,21 +5,38 @@ import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; + import SettingSelect from './SettingSelect.svelte'; + import { ReleaseChannel } from '@immich/sdk'; const disabled = $derived(featureFlagsManager.value.configFile); + const config = $derived(systemConfigManager.value); let configToEdit = $state(systemConfigManager.cloneValue());
event.preventDefault()}> -
+
+
diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index 03a053fcd5..dceb340505 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -8,7 +8,8 @@ import type { PageLoad } from './$types'; export const load = (async ({ parent, url }) => { await parent(); - const continueUrl = url.searchParams.get('continue') || Route.photos(); + const continueUrl = Route.continue(url.searchParams.get('continue'), Route.photos()); + if (authManager.authenticated) { redirect(307, continueUrl); } diff --git a/web/src/routes/auth/pin-prompt/+page.svelte b/web/src/routes/auth/pin-prompt/+page.svelte index d6648889a8..fff02e054f 100644 --- a/web/src/routes/auth/pin-prompt/+page.svelte +++ b/web/src/routes/auth/pin-prompt/+page.svelte @@ -30,7 +30,7 @@ await new Promise((resolve) => setTimeout(resolve, 1000)); - await goto(data.continueUrl); + await goto(Route.continue(data.continueUrl, Route.photos())); } catch (error) { handleError(error, $t('wrong_pin_code')); isBadPinCode = true;