mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
Merge remote-tracking branch 'origin/main' into feature/sync_assets_trashed_state
This commit is contained in:
commit
f7573ae317
145 changed files with 11086 additions and 2627 deletions
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
|
|
@ -1 +1 @@
|
||||||
22.18.0
|
22.19.0
|
||||||
|
|
|
||||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
|
|
@ -34,3 +34,7 @@ The `/api/something` endpoint is now `/api/something-else`
|
||||||
- [ ] I have followed naming conventions/patterns in the surrounding code
|
- [ ] I have followed naming conventions/patterns in the surrounding code
|
||||||
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
|
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
|
||||||
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
|
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
|
||||||
|
|
||||||
|
## Please describe to which degree, if any, an LLM was used in creating this pull request.
|
||||||
|
|
||||||
|
...
|
||||||
|
|
|
||||||
14
Makefile
14
Makefile
|
|
@ -10,14 +10,14 @@ dev-update: prepare-volumes
|
||||||
dev-scale: prepare-volumes
|
dev-scale: prepare-volumes
|
||||||
@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
|
@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
|
||||||
|
|
||||||
dev-docs: prepare-volumes
|
dev-docs:
|
||||||
npm --prefix docs run start
|
npm --prefix docs run start
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e: prepare-volumes
|
e2e:
|
||||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
||||||
|
|
||||||
e2e-update: prepare-volumes
|
e2e-update:
|
||||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
e2e-down:
|
e2e-down:
|
||||||
|
|
@ -73,6 +73,8 @@ define safe_chown
|
||||||
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
|
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
|
||||||
true; \
|
true; \
|
||||||
else \
|
else \
|
||||||
|
STATUS=$$?; echo "Exit code: $$STATUS $(1)"; \
|
||||||
|
echo "$$STATUS $(1)"; \
|
||||||
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
|
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi;
|
fi;
|
||||||
|
|
@ -83,11 +85,13 @@ prepare-volumes:
|
||||||
@$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R))
|
@$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R))
|
||||||
ifneq ($(UPLOAD_LOCATION),)
|
ifneq ($(UPLOAD_LOCATION),)
|
||||||
ifeq ($(filter /%,$(UPLOAD_LOCATION)),)
|
ifeq ($(filter /%,$(UPLOAD_LOCATION)),)
|
||||||
@mkdir -p "docker/$(UPLOAD_LOCATION)"
|
@mkdir -p "docker/$(UPLOAD_LOCATION)/photos/upload"
|
||||||
@$(call safe_chown,docker/$(UPLOAD_LOCATION),)
|
@$(call safe_chown,docker/$(UPLOAD_LOCATION),)
|
||||||
|
@$(call safe_chown,docker/$(UPLOAD_LOCATION)/photos,-R)
|
||||||
else
|
else
|
||||||
@mkdir -p "$(UPLOAD_LOCATION)"
|
@mkdir -p "$(UPLOAD_LOCATION)/photos/upload"
|
||||||
@$(call safe_chown,$(UPLOAD_LOCATION),)
|
@$(call safe_chown,$(UPLOAD_LOCATION),)
|
||||||
|
@$(call safe_chown,$(UPLOAD_LOCATION)/photos,-R)
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
22.18.0
|
22.19.0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.86",
|
"version": "2.2.87",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.18.0",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
|
|
@ -69,6 +69,6 @@
|
||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.18.0"
|
"node": "22.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
22.18.0
|
22.19.0
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,6 @@
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.18.0"
|
"node": "22.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
|
|
@ -1,4 +1,8 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.141.0",
|
||||||
|
"url": "https://v1.141.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.140.1",
|
"label": "v1.140.1",
|
||||||
"url": "https://v1.140.1.archive.immich.app"
|
"url": "https://v1.140.1.archive.immich.app"
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
22.18.0
|
22.19.0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.140.1",
|
"version": "1.141.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.18.0",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"sharp": "^0.34.0",
|
"sharp": "^0.34.3",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|
@ -54,6 +54,6 @@
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.18.0"
|
"node": "22.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1417,6 +1417,8 @@
|
||||||
"open_the_search_filters": "Open the search filters",
|
"open_the_search_filters": "Open the search filters",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
|
"organize_into_albums": "Organize into albums",
|
||||||
|
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
|
||||||
"organize_your_library": "Organize your library",
|
"organize_your_library": "Organize your library",
|
||||||
"original": "original",
|
"original": "original",
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
|
|
@ -1557,6 +1559,7 @@
|
||||||
"purchase_server_description_2": "Supporter status",
|
"purchase_server_description_2": "Supporter status",
|
||||||
"purchase_server_title": "Server",
|
"purchase_server_title": "Server",
|
||||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||||
|
"query_asset_id": "Query Asset ID",
|
||||||
"queue_status": "Queuing {count}/{total}",
|
"queue_status": "Queuing {count}/{total}",
|
||||||
"rating": "Star rating",
|
"rating": "Star rating",
|
||||||
"rating_clear": "Clear rating",
|
"rating_clear": "Clear rating",
|
||||||
|
|
@ -1735,7 +1738,7 @@
|
||||||
"select_user_for_sharing_page_err_album": "Failed to create album",
|
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"selected_count": "{count, plural, other {# selected}}",
|
"selected_count": "{count, plural, other {# selected}}",
|
||||||
"selected_gps_coordinates": "selected gps coordinates",
|
"selected_gps_coordinates": "Selected GPS Coordinates",
|
||||||
"send_message": "Send message",
|
"send_message": "Send message",
|
||||||
"send_welcome_email": "Send welcome email",
|
"send_welcome_email": "Send welcome email",
|
||||||
"server_endpoint": "Server Endpoint",
|
"server_endpoint": "Server Endpoint",
|
||||||
|
|
@ -2077,6 +2080,7 @@
|
||||||
"view_next_asset": "View next asset",
|
"view_next_asset": "View next asset",
|
||||||
"view_previous_asset": "View previous asset",
|
"view_previous_asset": "View previous asset",
|
||||||
"view_qr_code": "View QR code",
|
"view_qr_code": "View QR code",
|
||||||
|
"view_similar_photos": "View similar photos",
|
||||||
"view_stack": "View Stack",
|
"view_stack": "View Stack",
|
||||||
"view_user": "View User",
|
"view_user": "View User",
|
||||||
"viewer_remove_from_stack": "Remove from Stack",
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
|
|
|
||||||
34
mise.lock
Normal file
34
mise.lock
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
[tools.dart]
|
||||||
|
version = "3.8.2"
|
||||||
|
backend = "asdf:dart"
|
||||||
|
|
||||||
|
[tools.flutter]
|
||||||
|
version = "3.32.8-stable"
|
||||||
|
backend = "asdf:flutter"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
|
version = "1.31.4"
|
||||||
|
backend = "github:CQLabs/homebrew-dcm"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm".platforms.linux-x64]
|
||||||
|
checksum = "blake3:e9df5b765df327e1248fccf2c6165a89d632a065667f99c01765bf3047b94955"
|
||||||
|
size = 8821083
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.31.4/dcm-linux-x64-release.zip"
|
||||||
|
|
||||||
|
[tools.node]
|
||||||
|
version = "22.18.0"
|
||||||
|
backend = "core:node"
|
||||||
|
|
||||||
|
[tools.node.platforms.linux-x64]
|
||||||
|
checksum = "sha256:a2e703725d8683be86bb5da967bf8272f4518bdaf10f21389e2b2c9eaeae8c8a"
|
||||||
|
size = 54824343
|
||||||
|
url = "https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.gz"
|
||||||
|
|
||||||
|
[tools.pnpm]
|
||||||
|
version = "10.14.0"
|
||||||
|
backend = "aqua:pnpm/pnpm"
|
||||||
|
|
||||||
|
[tools.pnpm.platforms.linux-x64]
|
||||||
|
checksum = "blake3:13dfa46b7173d3cad3bad60a756a492ecf0bce48b23eb9f793e7ccec5a09b46d"
|
||||||
|
size = 66231525
|
||||||
|
url = "https://github.com/pnpm/pnpm/releases/download/v10.14.0/pnpm-linux-x64"
|
||||||
312
mise.toml
Normal file
312
mise.toml
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
[tools]
|
||||||
|
node = "22.19.0"
|
||||||
|
flutter = "3.32.8"
|
||||||
|
pnpm = "10.14.0"
|
||||||
|
dart = "3.8.2"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
|
version = "1.31.4"
|
||||||
|
bin = "dcm"
|
||||||
|
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
experimental = true
|
||||||
|
lockfile = true
|
||||||
|
pin = true
|
||||||
|
|
||||||
|
# .github
|
||||||
|
[tasks."github:install"]
|
||||||
|
run = "pnpm install --filter github --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks."github:format"]
|
||||||
|
env._.path = "./.github/node_modules/.bin"
|
||||||
|
dir = ".github"
|
||||||
|
run = "prettier --check ."
|
||||||
|
|
||||||
|
[tasks."github:format-fix"]
|
||||||
|
env._.path = "./.github/node_modules/.bin"
|
||||||
|
dir = ".github"
|
||||||
|
run = "prettier --write ."
|
||||||
|
|
||||||
|
# @immich/cli
|
||||||
|
[tasks."cli:install"]
|
||||||
|
run = "pnpm install --filter @immich/cli --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks."cli:build"]
|
||||||
|
env._.path = "./cli/node_modules/.bin"
|
||||||
|
dir = "cli"
|
||||||
|
run = "vite build"
|
||||||
|
|
||||||
|
[tasks."cli:test"]
|
||||||
|
env._.path = "./cli/node_modules/.bin"
|
||||||
|
dir = "cli"
|
||||||
|
run = "vite"
|
||||||
|
|
||||||
|
[tasks."cli:lint"]
|
||||||
|
env._.path = "./cli/node_modules/.bin"
|
||||||
|
dir = "cli"
|
||||||
|
run = "eslint \"src/**/*.ts\" --max-warnings 0"
|
||||||
|
|
||||||
|
[tasks."cli:lint-fix"]
|
||||||
|
run = "mise run cli:lint --fix"
|
||||||
|
|
||||||
|
[tasks."cli:format"]
|
||||||
|
env._.path = "./cli/node_modules/.bin"
|
||||||
|
dir = "cli"
|
||||||
|
run = "prettier --check ."
|
||||||
|
|
||||||
|
[tasks."cli:format-fix"]
|
||||||
|
env._.path = "./cli/node_modules/.bin"
|
||||||
|
dir = "cli"
|
||||||
|
run = "prettier --write ."
|
||||||
|
|
||||||
|
[tasks."cli:check"]
|
||||||
|
env._.path = "./cli/node_modules/.bin"
|
||||||
|
dir = "cli"
|
||||||
|
run = "tsc --noEmit"
|
||||||
|
|
||||||
|
# @immich/sdk
|
||||||
|
[tasks."sdk:install"]
|
||||||
|
run = "pnpm install --filter @immich/sdk --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks."sdk:build"]
|
||||||
|
env._.path = "./open-api/typescript-sdk/node_modules/.bin"
|
||||||
|
dir = "./open-api/typescript-sdk"
|
||||||
|
run = "tsc"
|
||||||
|
|
||||||
|
# docs
|
||||||
|
[tasks."docs:install"]
|
||||||
|
run = "pnpm install --filter documentation --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks."docs:start"]
|
||||||
|
env._.path = "./docs/node_modules/.bin"
|
||||||
|
dir = "docs"
|
||||||
|
run = "docusaurus --port 3005"
|
||||||
|
|
||||||
|
[tasks."docs:build"]
|
||||||
|
env._.path = "./docs/node_modules/.bin"
|
||||||
|
dir = "docs"
|
||||||
|
run = [
|
||||||
|
"jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
||||||
|
"docusaurus build",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[tasks."docs:preview"]
|
||||||
|
env._.path = "./docs/node_modules/.bin"
|
||||||
|
dir = "docs"
|
||||||
|
run = "docusaurus serve"
|
||||||
|
|
||||||
|
|
||||||
|
[tasks."docs:format"]
|
||||||
|
env._.path = "./docs/node_modules/.bin"
|
||||||
|
dir = "docs"
|
||||||
|
run = "prettier --check ."
|
||||||
|
|
||||||
|
[tasks."docs:format-fix"]
|
||||||
|
env._.path = "./docs/node_modules/.bin"
|
||||||
|
dir = "docs"
|
||||||
|
run = "prettier --write ."
|
||||||
|
|
||||||
|
|
||||||
|
# e2e
|
||||||
|
[tasks."e2e:install"]
|
||||||
|
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks."e2e:test"]
|
||||||
|
env._.path = "./e2e/node_modules/.bin"
|
||||||
|
dir = "e2e"
|
||||||
|
run = "vitest --run"
|
||||||
|
|
||||||
|
[tasks."e2e:test-web"]
|
||||||
|
env._.path = "./e2e/node_modules/.bin"
|
||||||
|
dir = "e2e"
|
||||||
|
run = "playwright test"
|
||||||
|
|
||||||
|
[tasks."e2e:format"]
|
||||||
|
env._.path = "./e2e/node_modules/.bin"
|
||||||
|
dir = "e2e"
|
||||||
|
run = "prettier --check ."
|
||||||
|
|
||||||
|
[tasks."e2e:format-fix"]
|
||||||
|
env._.path = "./e2e/node_modules/.bin"
|
||||||
|
dir = "e2e"
|
||||||
|
run = "prettier --write ."
|
||||||
|
|
||||||
|
[tasks."e2e:lint"]
|
||||||
|
env._.path = "./e2e/node_modules/.bin"
|
||||||
|
dir = "e2e"
|
||||||
|
run = "eslint \"src/**/*.ts\" --max-warnings 0"
|
||||||
|
|
||||||
|
[tasks."e2e:lint-fix"]
|
||||||
|
run = "mise run e2e:lint --fix"
|
||||||
|
|
||||||
|
[tasks."e2e:check"]
|
||||||
|
env._.path = "./e2e/node_modules/.bin"
|
||||||
|
dir = "e2e"
|
||||||
|
run = "tsc --noEmit"
|
||||||
|
|
||||||
|
# i18n
|
||||||
|
[tasks."i18n:format"]
|
||||||
|
run = "mise run i18n:format-fix"
|
||||||
|
|
||||||
|
[tasks."i18n:format-fix"]
|
||||||
|
run = "pnpm dlx sort-json ./i18n/*.json"
|
||||||
|
|
||||||
|
|
||||||
|
# server
|
||||||
|
[tasks."server:install"]
|
||||||
|
run = "pnpm install --filter immich --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks."server:build"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "nest build"
|
||||||
|
|
||||||
|
[tasks."server:test"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "vitest --config test/vitest.config.mjs"
|
||||||
|
|
||||||
|
[tasks."server:test-medium"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "vitest --config test/vitest.config.medium.mjs"
|
||||||
|
|
||||||
|
[tasks."server:format"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "prettier --check ."
|
||||||
|
|
||||||
|
[tasks."server:format-fix"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "prettier --write ."
|
||||||
|
|
||||||
|
[tasks."server:lint"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0"
|
||||||
|
|
||||||
|
[tasks."server:lint-fix"]
|
||||||
|
run = "mise run server:lint --fix"
|
||||||
|
|
||||||
|
[tasks."server:check"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "tsc --noEmit"
|
||||||
|
|
||||||
|
[tasks."server:sql"]
|
||||||
|
dir = "server"
|
||||||
|
run = "node ./dist/bin/sync-open-api.js"
|
||||||
|
|
||||||
|
[tasks."server:open-api"]
|
||||||
|
dir = "server"
|
||||||
|
run = "node ./dist/bin/sync-open-api.js"
|
||||||
|
|
||||||
|
[tasks."server:migrations"]
|
||||||
|
dir = "server"
|
||||||
|
run = "node ./dist/bin/migrations.js"
|
||||||
|
description = "Run database migration commands (create, generate, run, debug, or query)"
|
||||||
|
|
||||||
|
[tasks."server:schema-drop"]
|
||||||
|
run = "mise run server:migrations query 'DROP schema public cascade; CREATE schema public;'"
|
||||||
|
|
||||||
|
[tasks."server:schema-reset"]
|
||||||
|
run = "mise run server:schema-drop && mise run server:migrations run"
|
||||||
|
|
||||||
|
[tasks."server:email-dev"]
|
||||||
|
env._.path = "./server/node_modules/.bin"
|
||||||
|
dir = "server"
|
||||||
|
run = "email dev -p 3050 --dir src/emails"
|
||||||
|
|
||||||
|
[tasks."server:checklist"]
|
||||||
|
run = [
|
||||||
|
"mise run server:install",
|
||||||
|
"mise run server:format",
|
||||||
|
"mise run server:lint",
|
||||||
|
"mise run server:check",
|
||||||
|
"mise run server:test-medium --run",
|
||||||
|
"mise run server:test --run",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# web
|
||||||
|
[tasks."web:install"]
|
||||||
|
run = "pnpm install --filter immich-web --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks."web:svelte-kit-sync"]
|
||||||
|
env._.path = "./web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "svelte-kit sync"
|
||||||
|
|
||||||
|
[tasks."web:build"]
|
||||||
|
env._.path = "./web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "vite build"
|
||||||
|
|
||||||
|
[tasks."web:build-stats"]
|
||||||
|
env.BUILD_STATS = "true"
|
||||||
|
env._.path = "./web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "vite build"
|
||||||
|
|
||||||
|
[tasks."web:preview"]
|
||||||
|
env._.path = "./web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "vite preview"
|
||||||
|
|
||||||
|
[tasks."web:start"]
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "vite dev --host 0.0.0.0 --port 3000"
|
||||||
|
|
||||||
|
[tasks."web:test"]
|
||||||
|
depends = "web:svelte-kit-sync"
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "vitest"
|
||||||
|
|
||||||
|
[tasks."web:format"]
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "prettier --check ."
|
||||||
|
|
||||||
|
[tasks."web:format-fix"]
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "prettier --write ."
|
||||||
|
|
||||||
|
[tasks."web:lint"]
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "eslint . --max-warnings 0"
|
||||||
|
|
||||||
|
[tasks."web:lint-p"]
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "eslint-p . --max-warnings 0 --concurrency=4"
|
||||||
|
|
||||||
|
[tasks."web:lint-fix"]
|
||||||
|
run = "mise run web:lint --fix"
|
||||||
|
|
||||||
|
[tasks."web:check"]
|
||||||
|
depends = "web:svelte-kit-sync"
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "tsc --noEmit"
|
||||||
|
|
||||||
|
[tasks."web:check-svelte"]
|
||||||
|
depends = "web:svelte-kit-sync"
|
||||||
|
env._.path = "web/node_modules/.bin"
|
||||||
|
dir = "web"
|
||||||
|
run = "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore' --ignore src/lib/components/photos-page/asset-grid.svelte"
|
||||||
|
|
||||||
|
[tasks."web:checklist"]
|
||||||
|
run = [
|
||||||
|
"mise run web:install",
|
||||||
|
"mise run web:format",
|
||||||
|
"mise run web:check",
|
||||||
|
"mise run web:test --run",
|
||||||
|
"mise run web:lint",
|
||||||
|
]
|
||||||
|
|
@ -61,9 +61,8 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface BackgroundWorkerFgHostApi {
|
interface BackgroundWorkerFgHostApi {
|
||||||
fun enableSyncWorker()
|
fun enable()
|
||||||
fun enableUploadWorker()
|
fun disable()
|
||||||
fun disableUploadWorker()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by BackgroundWorkerFgHostApi. */
|
/** The codec used by BackgroundWorkerFgHostApi. */
|
||||||
|
|
@ -75,11 +74,11 @@ interface BackgroundWorkerFgHostApi {
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
api.enableSyncWorker()
|
api.enable()
|
||||||
listOf(null)
|
listOf(null)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
|
|
@ -91,27 +90,11 @@ interface BackgroundWorkerFgHostApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
api.enableUploadWorker()
|
api.disable()
|
||||||
listOf(null)
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
|
||||||
}
|
|
||||||
reply.reply(wrapped)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
val wrapped: List<Any?> = try {
|
|
||||||
api.disableUploadWorker()
|
|
||||||
listOf(null)
|
listOf(null)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
|
|
@ -182,23 +165,6 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
|
||||||
BackgroundWorkerPigeonCodec()
|
BackgroundWorkerPigeonCodec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun onLocalSync(maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
|
||||||
{
|
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
|
||||||
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$separatedMessageChannelSuffix"
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
|
||||||
channel.send(listOf(maxSecondsArg)) {
|
|
||||||
if (it is List<*>) {
|
|
||||||
if (it.size > 1) {
|
|
||||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
|
||||||
} else {
|
|
||||||
callback(Result.success(Unit))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||||
{
|
{
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,6 @@ import io.flutter.embedding.engine.loader.FlutterLoader
|
||||||
|
|
||||||
private const val TAG = "BackgroundWorker"
|
private const val TAG = "BackgroundWorker"
|
||||||
|
|
||||||
enum class BackgroundTaskType {
|
|
||||||
LOCAL_SYNC,
|
|
||||||
UPLOAD,
|
|
||||||
}
|
|
||||||
|
|
||||||
class BackgroundWorker(context: Context, params: WorkerParameters) :
|
class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
|
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
@ -84,13 +79,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
* This method acts as a bridge between the native Android background task system and Flutter.
|
* This method acts as a bridge between the native Android background task system and Flutter.
|
||||||
*/
|
*/
|
||||||
override fun onInitialized() {
|
override fun onInitialized() {
|
||||||
val taskTypeIndex = inputData.getInt(BackgroundWorkerApiImpl.WORKER_DATA_TASK_TYPE, 0)
|
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
||||||
val taskType = BackgroundTaskType.entries[taskTypeIndex]
|
|
||||||
|
|
||||||
when (taskType) {
|
|
||||||
BackgroundTaskType.LOCAL_SYNC -> flutterApi?.onLocalSync(null) { handleHostResult(it) }
|
|
||||||
BackgroundTaskType.UPLOAD -> flutterApi?.onAndroidUpload { handleHostResult(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
|
|
@ -141,8 +130,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
* - Parameter success: Indicates whether the background task completed successfully
|
* - Parameter success: Indicates whether the background task completed successfully
|
||||||
*/
|
*/
|
||||||
private fun complete(success: Result) {
|
private fun complete(success: Result) {
|
||||||
|
Log.d(TAG, "About to complete BackupWorker with result: $success")
|
||||||
isComplete = true
|
isComplete = true
|
||||||
engine?.destroy()
|
engine?.destroy()
|
||||||
|
engine = null
|
||||||
flutterApi = null
|
flutterApi = null
|
||||||
completionHandler.set(success)
|
completionHandler.set(success)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,8 @@ package app.alextran.immich.background
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
|
@ -16,18 +14,13 @@ private const val TAG = "BackgroundUploadImpl"
|
||||||
|
|
||||||
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
override fun enableSyncWorker() {
|
|
||||||
|
override fun enable() {
|
||||||
enqueueMediaObserver(ctx)
|
enqueueMediaObserver(ctx)
|
||||||
Log.i(TAG, "Scheduled media observer")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enableUploadWorker() {
|
override fun disable() {
|
||||||
updateUploadEnabled(ctx, true)
|
WorkManager.getInstance(ctx).cancelUniqueWork(OBSERVER_WORKER_NAME)
|
||||||
Log.i(TAG, "Scheduled background upload tasks")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disableUploadWorker() {
|
|
||||||
updateUploadEnabled(ctx, false)
|
|
||||||
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||||
Log.i(TAG, "Cancelled background upload tasks")
|
Log.i(TAG, "Cancelled background upload tasks")
|
||||||
}
|
}
|
||||||
|
|
@ -36,25 +29,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||||
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
||||||
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
||||||
|
|
||||||
const val WORKER_DATA_TASK_TYPE = "taskType"
|
|
||||||
|
|
||||||
const val SHARED_PREF_NAME = "Immich::Background"
|
|
||||||
const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled"
|
|
||||||
|
|
||||||
private fun updateUploadEnabled(context: Context, enabled: Boolean) {
|
|
||||||
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit {
|
|
||||||
putBoolean(SHARED_PREF_BACKUP_ENABLED, enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enqueueMediaObserver(ctx: Context) {
|
fun enqueueMediaObserver(ctx: Context) {
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
.setTriggerContentUpdateDelay(5, TimeUnit.SECONDS)
|
.setTriggerContentUpdateDelay(30, TimeUnit.SECONDS)
|
||||||
.setTriggerContentMaxDelay(1, TimeUnit.MINUTES)
|
.setTriggerContentMaxDelay(3, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
||||||
|
|
@ -66,15 +48,13 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||||
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
|
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enqueueBackgroundWorker(ctx: Context, taskType: BackgroundTaskType) {
|
fun enqueueBackgroundWorker(ctx: Context) {
|
||||||
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
||||||
|
|
||||||
val data = Data.Builder()
|
|
||||||
data.putInt(WORKER_DATA_TASK_TYPE, taskType.ordinal)
|
|
||||||
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
||||||
.setInputData(data.build()).build()
|
.build()
|
||||||
WorkManager.getInstance(ctx)
|
WorkManager.getInstance(ctx)
|
||||||
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
|
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,29 +6,17 @@ import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
|
||||||
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
|
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
Log.i("MediaObserver", "Content change detected, starting background worker")
|
Log.i("MediaObserver", "Content change detected, starting background worker")
|
||||||
|
// Re-enqueue itself to listen for future changes
|
||||||
|
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
||||||
|
|
||||||
// Enqueue backup worker only if there are new media changes
|
// Enqueue backup worker only if there are new media changes
|
||||||
if (triggeredContentUris.isNotEmpty()) {
|
if (triggeredContentUris.isNotEmpty()) {
|
||||||
val type =
|
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
|
||||||
if (isBackupEnabled(ctx)) BackgroundTaskType.UPLOAD else BackgroundTaskType.LOCAL_SYNC
|
|
||||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-enqueue itself to listen for future changes
|
|
||||||
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isBackupEnabled(context: Context): Boolean {
|
|
||||||
val prefs =
|
|
||||||
context.getSharedPreferences(
|
|
||||||
BackgroundWorkerApiImpl.SHARED_PREF_NAME,
|
|
||||||
Context.MODE_PRIVATE
|
|
||||||
)
|
|
||||||
return prefs.getBoolean(BackgroundWorkerApiImpl.SHARED_PREF_BACKUP_ENABLED, false)
|
|
||||||
}
|
}
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ platform :android do
|
||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 3011,
|
"android.injected.version.code" => 3012,
|
||||||
"android.injected.version.name" => "1.140.1",
|
"android.injected.version.name" => "1.141.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|
|
||||||
1
mobile/drift_schemas/main/drift_schema_v9.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v9.json
generated
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
|
@ -507,14 +507,10 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
|
@ -543,14 +539,10 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,8 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
|
||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol BackgroundWorkerFgHostApi {
|
protocol BackgroundWorkerFgHostApi {
|
||||||
func enableSyncWorker() throws
|
func enable() throws
|
||||||
func enableUploadWorker() throws
|
func disable() throws
|
||||||
func disableUploadWorker() throws
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
|
@ -84,44 +83,31 @@ class BackgroundWorkerFgHostApiSetup {
|
||||||
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
|
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
|
||||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
let enableSyncWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
enableSyncWorkerChannel.setMessageHandler { _, reply in
|
enableChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
try api.enableSyncWorker()
|
try api.enable()
|
||||||
reply(wrapResult(nil))
|
reply(wrapResult(nil))
|
||||||
} catch {
|
} catch {
|
||||||
reply(wrapError(error))
|
reply(wrapError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enableSyncWorkerChannel.setMessageHandler(nil)
|
enableChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
enableUploadWorkerChannel.setMessageHandler { _, reply in
|
disableChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
try api.enableUploadWorker()
|
try api.disable()
|
||||||
reply(wrapResult(nil))
|
reply(wrapResult(nil))
|
||||||
} catch {
|
} catch {
|
||||||
reply(wrapError(error))
|
reply(wrapError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enableUploadWorkerChannel.setMessageHandler(nil)
|
disableChannel.setMessageHandler(nil)
|
||||||
}
|
|
||||||
let disableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
disableUploadWorkerChannel.setMessageHandler { _, reply in
|
|
||||||
do {
|
|
||||||
try api.disableUploadWorker()
|
|
||||||
reply(wrapResult(nil))
|
|
||||||
} catch {
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
disableUploadWorkerChannel.setMessageHandler(nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +153,6 @@ class BackgroundWorkerBgHostApiSetup {
|
||||||
}
|
}
|
||||||
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||||
protocol BackgroundWorkerFlutterApiProtocol {
|
protocol BackgroundWorkerFlutterApiProtocol {
|
||||||
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
|
||||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
|
|
@ -182,24 +167,6 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
||||||
var codec: BackgroundWorkerPigeonCodec {
|
var codec: BackgroundWorkerPigeonCodec {
|
||||||
return BackgroundWorkerPigeonCodec.shared
|
return BackgroundWorkerPigeonCodec.shared
|
||||||
}
|
}
|
||||||
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
|
||||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync\(messageChannelSuffix)"
|
|
||||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
channel.sendMessage([maxSecondsArg] as [Any?]) { response in
|
|
||||||
guard let listResponse = response as? [Any?] else {
|
|
||||||
completion(.failure(createConnectionError(withChannelName: channelName)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if listResponse.count > 1 {
|
|
||||||
let code: String = listResponse[0] as! String
|
|
||||||
let message: String? = nilOrValue(listResponse[1])
|
|
||||||
let details: String? = nilOrValue(listResponse[2])
|
|
||||||
completion(.failure(PigeonError(code: code, message: message, details: details)))
|
|
||||||
} else {
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
|
||||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
enum BackgroundTaskType { case refresh, processing }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* DEBUG: Testing Background Tasks in Xcode
|
* DEBUG: Testing Background Tasks in Xcode
|
||||||
|
|
@ -10,10 +10,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
||||||
* 1. Pause the application in Xcode debugger
|
* 1. Pause the application in Xcode debugger
|
||||||
* 2. In the debugger console, enter one of the following commands:
|
* 2. In the debugger console, enter one of the following commands:
|
||||||
|
|
||||||
## For local sync (short-running sync):
|
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
|
||||||
|
|
||||||
## For background refresh (short-running sync):
|
## For background refresh (short-running sync):
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
||||||
|
|
@ -24,8 +20,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
||||||
|
|
||||||
* To simulate task expiration (useful for testing expiration handlers):
|
* To simulate task expiration (useful for testing expiration handlers):
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
||||||
|
|
@ -120,17 +114,9 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||||
* This method acts as a bridge between the native iOS background task system and Flutter.
|
* This method acts as a bridge between the native iOS background task system and Flutter.
|
||||||
*/
|
*/
|
||||||
func onInitialized() throws {
|
func onInitialized() throws {
|
||||||
switch self.taskType {
|
flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||||
case .refreshUpload, .processingUpload:
|
self.handleHostResult(result: result)
|
||||||
flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload,
|
})
|
||||||
maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
|
||||||
self.handleHostResult(result: result)
|
|
||||||
})
|
|
||||||
case .localSync:
|
|
||||||
flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
|
||||||
self.handleHostResult(result: result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -155,6 +141,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the result from Flutter API calls and determines the success/failure status.
|
* Handles the result from Flutter API calls and determines the success/failure status.
|
||||||
* Converts Flutter's Result type to a simple boolean success indicator for task completion.
|
* Converts Flutter's Result type to a simple boolean success indicator for task completion.
|
||||||
|
|
@ -177,6 +164,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||||
* - Parameter success: Indicates whether the background task completed successfully
|
* - Parameter success: Indicates whether the background task completed successfully
|
||||||
*/
|
*/
|
||||||
private func complete(success: Bool) {
|
private func complete(success: Bool) {
|
||||||
|
if(isComplete) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isComplete = true
|
isComplete = true
|
||||||
engine.destroyContext()
|
engine.destroyContext()
|
||||||
completionHandler(success)
|
completionHandler(success)
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,40 @@
|
||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
|
|
||||||
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||||
func enableSyncWorker() throws {
|
|
||||||
BackgroundWorkerApiImpl.scheduleLocalSync()
|
func enable() throws {
|
||||||
print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled")
|
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
||||||
|
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
||||||
|
print("BackgroundUploadImpl:enbale Background worker scheduled")
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableUploadWorker() throws {
|
func disable() throws {
|
||||||
BackgroundWorkerApiImpl.updateUploadEnabled(true)
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
||||||
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
|
||||||
BackgroundWorkerApiImpl.scheduleRefreshUpload()
|
print("BackgroundUploadImpl:disableUploadWorker Disabled background workers")
|
||||||
BackgroundWorkerApiImpl.scheduleProcessingUpload()
|
|
||||||
print("BackgroundUploadImpl:enableUploadWorker Scheduled background upload tasks")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func disableUploadWorker() throws {
|
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
|
||||||
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
|
||||||
BackgroundWorkerApiImpl.cancelUploadTasks()
|
|
||||||
print("BackgroundUploadImpl:disableUploadWorker Disabled background upload tasks")
|
|
||||||
}
|
|
||||||
|
|
||||||
public static let backgroundUploadEnabledKey = "immich:background:backup:enabled"
|
|
||||||
|
|
||||||
private static let localSyncTaskID = "app.alextran.immich.background.localSync"
|
|
||||||
private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload"
|
|
||||||
private static let processingUploadTaskID = "app.alextran.immich.background.processingUpload"
|
|
||||||
|
|
||||||
private static func updateUploadEnabled(_ isEnabled: Bool) {
|
|
||||||
return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func cancelUploadTasks() {
|
|
||||||
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID);
|
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func registerBackgroundWorkers() {
|
public static func registerBackgroundWorkers() {
|
||||||
BGTaskScheduler.shared.register(
|
BGTaskScheduler.shared.register(
|
||||||
forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in
|
forTaskWithIdentifier: processingTaskID, using: nil) { task in
|
||||||
if task is BGProcessingTask {
|
if task is BGProcessingTask {
|
||||||
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BGTaskScheduler.shared.register(
|
BGTaskScheduler.shared.register(
|
||||||
forTaskWithIdentifier: refreshUploadTaskID, using: nil) { task in
|
forTaskWithIdentifier: refreshTaskID, using: nil) { task in
|
||||||
if task is BGAppRefreshTask {
|
if task is BGAppRefreshTask {
|
||||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .refreshUpload)
|
handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BGTaskScheduler.shared.register(
|
|
||||||
forTaskWithIdentifier: localSyncTaskID, using: nil) { task in
|
|
||||||
if task is BGAppRefreshTask {
|
|
||||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .localSync)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func scheduleLocalSync() {
|
private static func scheduleRefreshWorker() {
|
||||||
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: localSyncTaskID)
|
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID)
|
||||||
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
|
||||||
|
|
||||||
do {
|
|
||||||
try BGTaskScheduler.shared.submit(backgroundRefresh)
|
|
||||||
} catch {
|
|
||||||
print("Could not schedule the local sync task \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func scheduleRefreshUpload() {
|
|
||||||
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshUploadTaskID)
|
|
||||||
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
@ -81,8 +44,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func scheduleProcessingUpload() {
|
private static func scheduleProcessingWorker() {
|
||||||
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingUploadTaskID)
|
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID)
|
||||||
|
|
||||||
backgroundProcessing.requiresNetworkConnectivity = true
|
backgroundProcessing.requiresNetworkConnectivity = true
|
||||||
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
|
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
|
||||||
|
|
@ -94,29 +57,16 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) {
|
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
||||||
let maxSeconds: Int?
|
scheduleRefreshWorker()
|
||||||
|
|
||||||
switch taskType {
|
|
||||||
case .localSync:
|
|
||||||
maxSeconds = 15
|
|
||||||
scheduleLocalSync()
|
|
||||||
case .refreshUpload:
|
|
||||||
maxSeconds = 20
|
|
||||||
scheduleRefreshUpload()
|
|
||||||
case .processingUpload:
|
|
||||||
print("Unexpected background refresh task encountered")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
||||||
runBackgroundWorker(task: task, taskType: taskType, maxSeconds: maxSeconds)
|
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||||
scheduleProcessingUpload()
|
scheduleProcessingWorker()
|
||||||
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
||||||
runBackgroundWorker(task: task, taskType: .processingUpload, maxSeconds: nil)
|
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,23 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||||
assetCache.countLimit = 10000
|
assetCache.countLimit = 10000
|
||||||
return assetCache
|
return assetCache
|
||||||
}()
|
}()
|
||||||
|
private static let activitySemaphore = DispatchSemaphore(value: 1)
|
||||||
|
private static let willResignActiveObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: UIApplication.willResignActiveNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { _ in
|
||||||
|
processingQueue.suspend()
|
||||||
|
activitySemaphore.wait()
|
||||||
|
}
|
||||||
|
private static let didBecomeActiveObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { _ in
|
||||||
|
processingQueue.resume()
|
||||||
|
activitySemaphore.signal()
|
||||||
|
}
|
||||||
|
|
||||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
||||||
Self.processingQueue.async {
|
Self.processingQueue.async {
|
||||||
|
|
@ -53,6 +70,7 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||||
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
||||||
|
|
||||||
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
||||||
|
self.waitForActiveState()
|
||||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
|
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +160,7 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||||
return completion(Self.cancelledResult)
|
return completion(Self.cancelledResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.waitForActiveState()
|
||||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
||||||
Self.removeRequest(requestId: requestId)
|
Self.removeRequest(requestId: requestId)
|
||||||
}
|
}
|
||||||
|
|
@ -184,4 +203,9 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||||
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
||||||
return asset
|
return asset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitForActiveState() {
|
||||||
|
Self.activitySemaphore.wait()
|
||||||
|
Self.activitySemaphore.signal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,190 +1,189 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app.alextran.immich.background.localSync</string>
|
<string>app.alextran.immich.background.refreshUpload</string>
|
||||||
<string>app.alextran.immich.background.refreshUpload</string>
|
<string>app.alextran.immich.background.processingUpload</string>
|
||||||
<string>app.alextran.immich.background.processingUpload</string>
|
<string>app.alextran.immich.backgroundFetch</string>
|
||||||
<string>app.alextran.immich.backgroundFetch</string>
|
<string>app.alextran.immich.backgroundProcessing</string>
|
||||||
<string>app.alextran.immich.backgroundProcessing</string>
|
</array>
|
||||||
</array>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<true/>
|
||||||
<true />
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<key>CFBundleDisplayName</key>
|
||||||
<key>CFBundleDisplayName</key>
|
<string>${PRODUCT_NAME}</string>
|
||||||
<string>${PRODUCT_NAME}</string>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<array>
|
||||||
<array>
|
<dict>
|
||||||
<dict>
|
<key>CFBundleTypeName</key>
|
||||||
<key>CFBundleTypeName</key>
|
<string>ShareHandler</string>
|
||||||
<string>ShareHandler</string>
|
<key>LSHandlerRank</key>
|
||||||
<key>LSHandlerRank</key>
|
<string>Alternate</string>
|
||||||
<string>Alternate</string>
|
<key>LSItemContentTypes</key>
|
||||||
<key>LSItemContentTypes</key>
|
<array>
|
||||||
<array>
|
<string>public.file-url</string>
|
||||||
<string>public.file-url</string>
|
<string>public.image</string>
|
||||||
<string>public.image</string>
|
<string>public.text</string>
|
||||||
<string>public.text</string>
|
<string>public.movie</string>
|
||||||
<string>public.movie</string>
|
<string>public.url</string>
|
||||||
<string>public.url</string>
|
<string>public.data</string>
|
||||||
<string>public.data</string>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
</dict>
|
</array>
|
||||||
</array>
|
<key>CFBundleExecutable</key>
|
||||||
<key>CFBundleExecutable</key>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<key>CFBundleIdentifier</key>
|
||||||
<key>CFBundleIdentifier</key>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<string>6.0</string>
|
||||||
<string>6.0</string>
|
<key>CFBundleLocalizations</key>
|
||||||
<key>CFBundleLocalizations</key>
|
<array>
|
||||||
<array>
|
<string>en</string>
|
||||||
<string>en</string>
|
<string>ar</string>
|
||||||
<string>ar</string>
|
<string>ca</string>
|
||||||
<string>ca</string>
|
<string>cs</string>
|
||||||
<string>cs</string>
|
<string>da</string>
|
||||||
<string>da</string>
|
<string>de</string>
|
||||||
<string>de</string>
|
<string>es</string>
|
||||||
<string>es</string>
|
<string>fi</string>
|
||||||
<string>fi</string>
|
<string>fr</string>
|
||||||
<string>fr</string>
|
<string>he</string>
|
||||||
<string>he</string>
|
<string>hi</string>
|
||||||
<string>hi</string>
|
<string>hu</string>
|
||||||
<string>hu</string>
|
<string>it</string>
|
||||||
<string>it</string>
|
<string>ja</string>
|
||||||
<string>ja</string>
|
<string>ko</string>
|
||||||
<string>ko</string>
|
<string>lv</string>
|
||||||
<string>lv</string>
|
<string>mn</string>
|
||||||
<string>mn</string>
|
<string>nb</string>
|
||||||
<string>nb</string>
|
<string>nl</string>
|
||||||
<string>nl</string>
|
<string>pl</string>
|
||||||
<string>pl</string>
|
<string>pt</string>
|
||||||
<string>pt</string>
|
<string>ro</string>
|
||||||
<string>ro</string>
|
<string>ru</string>
|
||||||
<string>ru</string>
|
<string>sk</string>
|
||||||
<string>sk</string>
|
<string>sl</string>
|
||||||
<string>sl</string>
|
<string>sr</string>
|
||||||
<string>sr</string>
|
<string>sv</string>
|
||||||
<string>sv</string>
|
<string>th</string>
|
||||||
<string>th</string>
|
<string>uk</string>
|
||||||
<string>uk</string>
|
<string>vi</string>
|
||||||
<string>vi</string>
|
<string>zh</string>
|
||||||
<string>zh</string>
|
</array>
|
||||||
</array>
|
<key>CFBundleName</key>
|
||||||
<key>CFBundleName</key>
|
<string>immich_mobile</string>
|
||||||
<string>immich_mobile</string>
|
<key>CFBundlePackageType</key>
|
||||||
<key>CFBundlePackageType</key>
|
<string>APPL</string>
|
||||||
<string>APPL</string>
|
<key>CFBundleShortVersionString</key>
|
||||||
<key>CFBundleShortVersionString</key>
|
<string>1.140.0</string>
|
||||||
<string>1.140.0</string>
|
<key>CFBundleSignature</key>
|
||||||
<key>CFBundleSignature</key>
|
<string>????</string>
|
||||||
<string>????</string>
|
<key>CFBundleURLTypes</key>
|
||||||
<key>CFBundleURLTypes</key>
|
<array>
|
||||||
<array>
|
<dict>
|
||||||
<dict>
|
<key>CFBundleTypeRole</key>
|
||||||
<key>CFBundleTypeRole</key>
|
<string>Editor</string>
|
||||||
<string>Editor</string>
|
<key>CFBundleURLName</key>
|
||||||
<key>CFBundleURLName</key>
|
<string>Share Extension</string>
|
||||||
<string>Share Extension</string>
|
<key>CFBundleURLSchemes</key>
|
||||||
<key>CFBundleURLSchemes</key>
|
<array>
|
||||||
<array>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
</dict>
|
<dict>
|
||||||
<dict>
|
<key>CFBundleTypeRole</key>
|
||||||
<key>CFBundleTypeRole</key>
|
<string>Editor</string>
|
||||||
<string>Editor</string>
|
<key>CFBundleURLName</key>
|
||||||
<key>CFBundleURLName</key>
|
<string>Deep Link</string>
|
||||||
<string>Deep Link</string>
|
<key>CFBundleURLSchemes</key>
|
||||||
<key>CFBundleURLSchemes</key>
|
<array>
|
||||||
<array>
|
<string>immich</string>
|
||||||
<string>immich</string>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
</dict>
|
</array>
|
||||||
</array>
|
<key>CFBundleVersion</key>
|
||||||
<key>CFBundleVersion</key>
|
<string>219</string>
|
||||||
<string>219</string>
|
<key>FLTEnableImpeller</key>
|
||||||
<key>FLTEnableImpeller</key>
|
<true/>
|
||||||
<true />
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<false/>
|
||||||
<false />
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<array>
|
||||||
<array>
|
<string>https</string>
|
||||||
<string>https</string>
|
</array>
|
||||||
</array>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<true/>
|
||||||
<true />
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<string>No</string>
|
||||||
<string>No</string>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<true/>
|
||||||
<true />
|
<key>NSAppTransportSecurity</key>
|
||||||
<key>NSAppTransportSecurity</key>
|
<dict>
|
||||||
<dict>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<true/>
|
||||||
<true />
|
</dict>
|
||||||
</dict>
|
<key>NSBonjourServices</key>
|
||||||
<key>NSBonjourServices</key>
|
<array>
|
||||||
<array>
|
<string>_googlecast._tcp</string>
|
||||||
<string>_googlecast._tcp</string>
|
<string>_CC1AD845._googlecast._tcp</string>
|
||||||
<string>_CC1AD845._googlecast._tcp</string>
|
</array>
|
||||||
</array>
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>NSCameraUsageDescription</key>
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<string>We need local network permission to connect to the local server using IP address and
|
||||||
<string>We need local network permission to connect to the local server using IP address and
|
|
||||||
allow the casting feature to work</string>
|
allow the casting feature to work</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||||
<key>NSLocationUsageDescription</key>
|
<key>NSLocationUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>processing</string>
|
<string>processing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false />
|
<false/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
<true />
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
@ -22,7 +22,7 @@ platform :ios do
|
||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
)
|
)
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.140.1"
|
version_number: "1.141.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ class LocalAlbum {
|
||||||
|
|
||||||
final int assetCount;
|
final int assetCount;
|
||||||
final BackupSelection backupSelection;
|
final BackupSelection backupSelection;
|
||||||
|
final String? linkedRemoteAlbumId;
|
||||||
|
|
||||||
const LocalAlbum({
|
const LocalAlbum({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -23,6 +24,7 @@ class LocalAlbum {
|
||||||
this.assetCount = 0,
|
this.assetCount = 0,
|
||||||
this.backupSelection = BackupSelection.none,
|
this.backupSelection = BackupSelection.none,
|
||||||
this.isIosSharedAlbum = false,
|
this.isIosSharedAlbum = false,
|
||||||
|
this.linkedRemoteAlbumId,
|
||||||
});
|
});
|
||||||
|
|
||||||
LocalAlbum copyWith({
|
LocalAlbum copyWith({
|
||||||
|
|
@ -32,6 +34,7 @@ class LocalAlbum {
|
||||||
int? assetCount,
|
int? assetCount,
|
||||||
BackupSelection? backupSelection,
|
BackupSelection? backupSelection,
|
||||||
bool? isIosSharedAlbum,
|
bool? isIosSharedAlbum,
|
||||||
|
String? linkedRemoteAlbumId,
|
||||||
}) {
|
}) {
|
||||||
return LocalAlbum(
|
return LocalAlbum(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -40,6 +43,7 @@ class LocalAlbum {
|
||||||
assetCount: assetCount ?? this.assetCount,
|
assetCount: assetCount ?? this.assetCount,
|
||||||
backupSelection: backupSelection ?? this.backupSelection,
|
backupSelection: backupSelection ?? this.backupSelection,
|
||||||
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +57,8 @@ class LocalAlbum {
|
||||||
other.updatedAt == updatedAt &&
|
other.updatedAt == updatedAt &&
|
||||||
other.assetCount == assetCount &&
|
other.assetCount == assetCount &&
|
||||||
other.backupSelection == backupSelection &&
|
other.backupSelection == backupSelection &&
|
||||||
other.isIosSharedAlbum == isIosSharedAlbum;
|
other.isIosSharedAlbum == isIosSharedAlbum &&
|
||||||
|
other.linkedRemoteAlbumId == linkedRemoteAlbumId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -63,7 +68,8 @@ class LocalAlbum {
|
||||||
updatedAt.hashCode ^
|
updatedAt.hashCode ^
|
||||||
assetCount.hashCode ^
|
assetCount.hashCode ^
|
||||||
backupSelection.hashCode ^
|
backupSelection.hashCode ^
|
||||||
isIosSharedAlbum.hashCode;
|
isIosSharedAlbum.hashCode ^
|
||||||
|
linkedRemoteAlbumId.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -75,6 +81,7 @@ updatedAt: $updatedAt,
|
||||||
assetCount: $assetCount,
|
assetCount: $assetCount,
|
||||||
backupSelection: $backupSelection,
|
backupSelection: $backupSelection,
|
||||||
isIosSharedAlbum: $isIosSharedAlbum
|
isIosSharedAlbum: $isIosSharedAlbum
|
||||||
|
linkedRemoteAlbumId: $linkedRemoteAlbumId,
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.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/logger_db.repository.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||||
|
|
@ -30,11 +31,9 @@ class BackgroundWorkerFgService {
|
||||||
const BackgroundWorkerFgService(this._foregroundHostApi);
|
const BackgroundWorkerFgService(this._foregroundHostApi);
|
||||||
|
|
||||||
// TODO: Move this call to native side once old timeline is removed
|
// TODO: Move this call to native side once old timeline is removed
|
||||||
Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker();
|
Future<void> enable() => _foregroundHostApi.enable();
|
||||||
|
|
||||||
Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker();
|
Future<void> disable() => _foregroundHostApi.disable();
|
||||||
|
|
||||||
Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
|
|
@ -43,7 +42,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
final Drift _drift;
|
final Drift _drift;
|
||||||
final DriftLogger _driftLogger;
|
final DriftLogger _driftLogger;
|
||||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||||
final Logger _logger = Logger('BackgroundWorkerBgService');
|
final Logger _logger = Logger('BackgroundUploadBgService');
|
||||||
|
late final IsolateLockManager _lockManager;
|
||||||
|
|
||||||
bool _isCleanedUp = false;
|
bool _isCleanedUp = false;
|
||||||
|
|
||||||
|
|
@ -59,6 +59,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
driftProvider.overrideWith(driftOverride(drift)),
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
|
||||||
BackgroundWorkerFlutterApi.setUp(this);
|
BackgroundWorkerFlutterApi.setUp(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,41 +83,31 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||||
await FileDownloader().trackTasks();
|
await FileDownloader().trackTasks();
|
||||||
configureFileDownloaderNotifications();
|
configureFileDownloaderNotifications();
|
||||||
|
|
||||||
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
|
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
|
||||||
|
|
||||||
// Notify the host that the background worker service has been initialized and is ready to use
|
// Notify the host that the background upload service has been initialized and is ready to use
|
||||||
_backgroundHostApi.onInitialized();
|
debugPrint("Acquiring background worker lock");
|
||||||
|
if (await _lockManager.acquireLock().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () {
|
||||||
|
_lockManager.cancel();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
)) {
|
||||||
|
_logger.info("Acquired background worker lock");
|
||||||
|
await _backgroundHostApi.onInitialized();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.warning("Failed to acquire background worker lock");
|
||||||
|
await _cleanup();
|
||||||
|
await _backgroundHostApi.close();
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Failed to initialize background worker", error, stack);
|
_logger.severe("Failed to initialize background worker", error, stack);
|
||||||
_backgroundHostApi.close();
|
_backgroundHostApi.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLocalSync(int? maxSeconds) async {
|
|
||||||
try {
|
|
||||||
_logger.info('Local background syncing started');
|
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
|
|
||||||
final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null;
|
|
||||||
await _syncAssets(hashTimeout: timeout, syncRemote: false);
|
|
||||||
|
|
||||||
sw.stop();
|
|
||||||
_logger.info("Local sync completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe("Failed to complete local sync", error, stack);
|
|
||||||
} finally {
|
|
||||||
await _cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We do the following on Android upload
|
|
||||||
* - Sync local assets
|
|
||||||
* - Hash local assets 3 / 6 minutes
|
|
||||||
* - Sync remote assets
|
|
||||||
* - Check and requeue upload tasks
|
|
||||||
*/
|
|
||||||
@override
|
@override
|
||||||
Future<void> onAndroidUpload() async {
|
Future<void> onAndroidUpload() async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -135,14 +126,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We do the following on background upload
|
|
||||||
* - Sync local assets
|
|
||||||
* - Hash local assets
|
|
||||||
* - Sync remote assets
|
|
||||||
* - Check and requeue upload tasks
|
|
||||||
*
|
|
||||||
* The native side will not send the maxSeconds value for processing tasks
|
|
||||||
*/
|
|
||||||
@override
|
@override
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -194,7 +177,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
await _drift.close();
|
await _drift.close();
|
||||||
await _driftLogger.close();
|
await _driftLogger.close();
|
||||||
_ref.dispose();
|
_ref.dispose();
|
||||||
debugPrint("Background worker cleaned up");
|
_lockManager.releaseLock();
|
||||||
|
_logger.info("Background worker resources cleaned up");
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
|
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +206,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async {
|
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
||||||
final futures = <Future<void>>[];
|
final futures = <Future<void>>[];
|
||||||
|
|
||||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||||
|
|
@ -244,10 +228,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
});
|
});
|
||||||
|
|
||||||
futures.add(localSyncFuture);
|
futures.add(localSyncFuture);
|
||||||
if (syncRemote) {
|
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
|
||||||
final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote();
|
|
||||||
futures.add(remoteSyncFuture);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.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/local_asset.repository.dart';
|
||||||
|
|
@ -35,6 +36,7 @@ class HashService {
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<void> hashAssets() async {
|
Future<void> hashAssets() async {
|
||||||
|
_log.info("Starting hashing of assets");
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
// Sorted by backupSelection followed by isCloud
|
// Sorted by backupSelection followed by isCloud
|
||||||
final localAlbums = await _localAlbumRepository.getAll(
|
final localAlbums = await _localAlbumRepository.getAll(
|
||||||
|
|
@ -49,7 +51,7 @@ class HashService {
|
||||||
|
|
||||||
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
||||||
if (assetsToHash.isNotEmpty) {
|
if (assetsToHash.isNotEmpty) {
|
||||||
await _hashAssets(assetsToHash);
|
await _hashAssets(album, assetsToHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +62,7 @@ class HashService {
|
||||||
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
||||||
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
||||||
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
||||||
Future<void> _hashAssets(List<LocalAsset> assetsToHash) async {
|
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash) async {
|
||||||
int bytesProcessed = 0;
|
int bytesProcessed = 0;
|
||||||
final toHash = <_AssetToPath>[];
|
final toHash = <_AssetToPath>[];
|
||||||
|
|
||||||
|
|
@ -72,6 +74,9 @@ class HashService {
|
||||||
|
|
||||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
_log.warning(
|
||||||
|
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt} from album: ${album.name}",
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,17 +84,17 @@ class HashService {
|
||||||
toHash.add(_AssetToPath(asset: asset, path: file.path));
|
toHash.add(_AssetToPath(asset: asset, path: file.path));
|
||||||
|
|
||||||
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
|
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
|
||||||
await _processBatch(toHash);
|
await _processBatch(album, toHash);
|
||||||
toHash.clear();
|
toHash.clear();
|
||||||
bytesProcessed = 0;
|
bytesProcessed = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _processBatch(toHash);
|
await _processBatch(album, toHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes a batch of assets.
|
/// Processes a batch of assets.
|
||||||
Future<void> _processBatch(List<_AssetToPath> toHash) async {
|
Future<void> _processBatch(LocalAlbum album, List<_AssetToPath> toHash) async {
|
||||||
if (toHash.isEmpty) {
|
if (toHash.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +119,9 @@ class HashService {
|
||||||
if (hash?.length == 20) {
|
if (hash?.length == 20) {
|
||||||
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||||
} else {
|
} else {
|
||||||
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
|
_log.warning(
|
||||||
|
"Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,16 @@ class LocalAlbumService {
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _repository.getCount();
|
return _repository.getCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> unlinkRemoteAlbum(String id) async {
|
||||||
|
return _repository.unlinkRemoteAlbum(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
|
||||||
|
return _repository.linkRemoteAlbum(localAlbumId, remoteAlbumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<LocalAlbum>> getBackupAlbums() {
|
||||||
|
return _repository.getBackupAlbums();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ class RemoteAlbumService {
|
||||||
return _repository.get(albumId);
|
return _repository.get(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||||
|
return _repository.getByName(albumName, ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> sortAlbums(
|
Future<List<RemoteAlbum>> sortAlbums(
|
||||||
List<RemoteAlbum> albums,
|
List<RemoteAlbum> albums,
|
||||||
RemoteAlbumSortMode sortMode, {
|
RemoteAlbumSortMode sortMode, {
|
||||||
|
|
@ -80,7 +84,6 @@ class RemoteAlbumService {
|
||||||
|
|
||||||
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
|
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
|
||||||
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
|
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
|
||||||
|
|
||||||
await _repository.create(album, assetIds);
|
await _repository.create(album, assetIds);
|
||||||
|
|
||||||
return album;
|
return album;
|
||||||
|
|
|
||||||
101
mobile/lib/domain/services/sync_linked_album.service.dart
Normal file
101
mobile/lib/domain/services/sync_linked_album.service.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
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/infrastructure/repositories/local_album.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
|
||||||
|
final syncLinkedAlbumServiceProvider = Provider(
|
||||||
|
(ref) => SyncLinkedAlbumService(
|
||||||
|
ref.watch(localAlbumRepository),
|
||||||
|
ref.watch(remoteAlbumRepository),
|
||||||
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class SyncLinkedAlbumService {
|
||||||
|
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||||
|
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||||
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
|
||||||
|
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||||
|
|
||||||
|
Future<void> syncLinkedAlbums(String userId) async {
|
||||||
|
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||||
|
|
||||||
|
await Future.wait(
|
||||||
|
selectedAlbums.map((localAlbum) async {
|
||||||
|
final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId;
|
||||||
|
if (linkedRemoteAlbumId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId);
|
||||||
|
if (remoteAlbum == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get assets that are uploaded but not in the remote album
|
||||||
|
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
|
||||||
|
|
||||||
|
if (assetIds.isNotEmpty) {
|
||||||
|
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
|
||||||
|
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
||||||
|
for (final album in localAlbums) {
|
||||||
|
await _processLocalAlbum(album, ownerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes a single local album to ensure proper linking with remote albums
|
||||||
|
Future<void> _processLocalAlbum(LocalAlbum localAlbum, String ownerId) {
|
||||||
|
final hasLinkedRemoteAlbum = localAlbum.linkedRemoteAlbumId != null;
|
||||||
|
|
||||||
|
if (hasLinkedRemoteAlbum) {
|
||||||
|
return _handleLinkedAlbum(localAlbum);
|
||||||
|
} else {
|
||||||
|
return _handleUnlinkedAlbum(localAlbum, ownerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles albums that are already linked to a remote album
|
||||||
|
Future<void> _handleLinkedAlbum(LocalAlbum localAlbum) async {
|
||||||
|
final remoteAlbumId = localAlbum.linkedRemoteAlbumId!;
|
||||||
|
final remoteAlbum = await _remoteAlbumRepository.get(remoteAlbumId);
|
||||||
|
|
||||||
|
final remoteAlbumExists = remoteAlbum != null;
|
||||||
|
if (!remoteAlbumExists) {
|
||||||
|
return _localAlbumRepository.unlinkRemoteAlbum(localAlbum.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles albums that are not linked to any remote album
|
||||||
|
Future<void> _handleUnlinkedAlbum(LocalAlbum localAlbum, String ownerId) async {
|
||||||
|
final existingRemoteAlbum = await _remoteAlbumRepository.getByName(localAlbum.name, ownerId);
|
||||||
|
|
||||||
|
if (existingRemoteAlbum != null) {
|
||||||
|
return _linkToExistingRemoteAlbum(localAlbum, existingRemoteAlbum);
|
||||||
|
} else {
|
||||||
|
return _createAndLinkNewRemoteAlbum(localAlbum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Links a local album to an existing remote album
|
||||||
|
Future<void> _linkToExistingRemoteAlbum(LocalAlbum localAlbum, dynamic existingRemoteAlbum) {
|
||||||
|
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, existingRemoteAlbum.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new remote album and links it to the local album
|
||||||
|
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
|
||||||
|
debugPrint("Creating new remote album for local album: ${localAlbum.name}");
|
||||||
|
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
|
||||||
|
await _remoteAlbumRepository.create(newRemoteAlbum, []);
|
||||||
|
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||||
import 'package:immich_mobile/utils/isolate.dart';
|
import 'package:immich_mobile/utils/isolate.dart';
|
||||||
import 'package:worker_manager/worker_manager.dart';
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
@ -155,6 +156,11 @@ class BackgroundSyncManager {
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> syncLinkedAlbum() {
|
||||||
|
final task = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
|
||||||
|
return task.future;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||||
|
|
|
||||||
235
mobile/lib/domain/utils/isolate_lock_manager.dart
Normal file
235
mobile/lib/domain/utils/isolate_lock_manager.dart
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
const String kIsolateLockManagerPort = "immich://isolate_mutex";
|
||||||
|
|
||||||
|
enum _LockStatus { active, released }
|
||||||
|
|
||||||
|
class _IsolateRequest {
|
||||||
|
const _IsolateRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HeartbeatRequest extends _IsolateRequest {
|
||||||
|
// Port for the receiver to send replies back
|
||||||
|
final SendPort sendPort;
|
||||||
|
|
||||||
|
const _HeartbeatRequest(this.sendPort);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'type': 'heartbeat', 'sendPort': sendPort};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CloseRequest extends _IsolateRequest {
|
||||||
|
const _CloseRequest();
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'type': 'close'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IsolateResponse {
|
||||||
|
const _IsolateResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HeartbeatResponse extends _IsolateResponse {
|
||||||
|
final _LockStatus status;
|
||||||
|
|
||||||
|
const _HeartbeatResponse(this.status);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'type': 'heartbeat', 'status': status.index};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef OnCloseLockHolderRequest = void Function();
|
||||||
|
|
||||||
|
class IsolateLockManager {
|
||||||
|
final String _portName;
|
||||||
|
bool _hasLock = false;
|
||||||
|
ReceivePort? _receivePort;
|
||||||
|
final OnCloseLockHolderRequest? _onCloseRequest;
|
||||||
|
final Set<SendPort> _waitingIsolates = {};
|
||||||
|
// Token object - a new one is created for each acquisition attempt
|
||||||
|
Object? _currentAcquisitionToken;
|
||||||
|
|
||||||
|
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
|
||||||
|
: _portName = portName ?? kIsolateLockManagerPort,
|
||||||
|
_onCloseRequest = onCloseRequest;
|
||||||
|
|
||||||
|
Future<bool> acquireLock() async {
|
||||||
|
if (_hasLock) {
|
||||||
|
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new token - this invalidates any previous attempt
|
||||||
|
final token = _currentAcquisitionToken = Object();
|
||||||
|
|
||||||
|
final ReceivePort rp = _receivePort = ReceivePort(_portName);
|
||||||
|
final SendPort sp = rp.sendPort;
|
||||||
|
|
||||||
|
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
|
||||||
|
// This attempt was superseded by a newer one in the same isolate
|
||||||
|
if (_currentAcquisitionToken != token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _lockReleasedByHolder(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasLock = true;
|
||||||
|
rp.listen(_onRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _lockReleasedByHolder(Object token) async {
|
||||||
|
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
|
||||||
|
debugPrint("Found lock holder: $holder");
|
||||||
|
if (holder == null) {
|
||||||
|
// No holder, try and acquire lock
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ReceivePort tempRp = ReceivePort();
|
||||||
|
final SendPort tempSp = tempRp.sendPort;
|
||||||
|
final bs = tempRp.asBroadcastStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
// Send a heartbeat request with the send port to receive reply from the holder
|
||||||
|
|
||||||
|
debugPrint("Sending heartbeat request to lock holder");
|
||||||
|
holder.send(_HeartbeatRequest(tempSp).toJson());
|
||||||
|
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
|
||||||
|
|
||||||
|
debugPrint("Received heartbeat response from lock holder: $answer");
|
||||||
|
// This attempt was superseded by a newer one in the same isolate
|
||||||
|
if (_currentAcquisitionToken != token) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answer == null) {
|
||||||
|
// Holder failed, most likely killed without calling releaseLock
|
||||||
|
// Check if a different waiting isolate took the lock
|
||||||
|
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
|
||||||
|
// No, remove the stale lock
|
||||||
|
IsolateNameServer.removePortNameMapping(_portName);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown message type received for heartbeat request. Try again
|
||||||
|
_IsolateResponse? response = _parseResponse(answer);
|
||||||
|
if (response == null || response is! _HeartbeatResponse) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status == _LockStatus.released) {
|
||||||
|
// Holder has released the lock
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the _LockStatus is active, we check again if the task completed
|
||||||
|
// by sending a released messaged again, if not, send a new heartbeat again
|
||||||
|
|
||||||
|
// Check if the holder completed its task after the heartbeat
|
||||||
|
answer = await bs.first.timeout(
|
||||||
|
const Duration(seconds: 3),
|
||||||
|
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
response = _parseResponse(answer);
|
||||||
|
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Timeout or error
|
||||||
|
} finally {
|
||||||
|
tempRp.close();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_IsolateRequest? _parseRequest(dynamic msg) {
|
||||||
|
if (msg is! Map<String, dynamic>) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (msg['type']) {
|
||||||
|
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
|
||||||
|
'close' => const _CloseRequest(),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_IsolateResponse? _parseResponse(dynamic msg) {
|
||||||
|
if (msg is! Map<String, dynamic>) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (msg['type']) {
|
||||||
|
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executed in the isolate with the lock
|
||||||
|
void _onRequest(dynamic msg) {
|
||||||
|
final request = _parseRequest(msg);
|
||||||
|
if (request == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request is _HeartbeatRequest) {
|
||||||
|
// Add the send port to the list of waiting isolates
|
||||||
|
_waitingIsolates.add(request.sendPort);
|
||||||
|
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request is _CloseRequest) {
|
||||||
|
_onCloseRequest?.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseLock() {
|
||||||
|
if (_hasLock) {
|
||||||
|
IsolateNameServer.removePortNameMapping(_portName);
|
||||||
|
|
||||||
|
// Notify waiting isolates
|
||||||
|
for (final port in _waitingIsolates) {
|
||||||
|
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
|
||||||
|
}
|
||||||
|
_waitingIsolates.clear();
|
||||||
|
|
||||||
|
_hasLock = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_receivePort?.close();
|
||||||
|
_receivePort = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
if (_hasLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("Cancelling ongoing acquire lock attempts");
|
||||||
|
// Create a new token to invalidate ongoing acquire lock attempts
|
||||||
|
_currentAcquisitionToken = Object();
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestHolderToClose() {
|
||||||
|
if (_hasLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
11
mobile/lib/domain/utils/sync_linked_album.dart
Normal file
11
mobile/lib/domain/utils/sync_linked_album.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||||
|
final user = ref.read(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||||
|
|
@ -11,9 +13,26 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||||
IntColumn get backupSelection => intEnum<BackupSelection>()();
|
IntColumn get backupSelection => intEnum<BackupSelection>()();
|
||||||
BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))();
|
BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
// // Linked album for putting assets to the remote album after finished uploading
|
||||||
|
TextColumn get linkedRemoteAlbumId =>
|
||||||
|
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.setNull).nullable()();
|
||||||
|
|
||||||
// Used for mark & sweep
|
// Used for mark & sweep
|
||||||
BoolColumn get marker_ => boolean().nullable()();
|
BoolColumn get marker_ => boolean().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
|
||||||
|
LocalAlbum toDto({int assetCount = 0}) {
|
||||||
|
return LocalAlbum(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
assetCount: assetCount,
|
||||||
|
backupSelection: backupSelection,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart' as i2;
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'
|
||||||
as i3;
|
as i3;
|
||||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||||
|
as i5;
|
||||||
|
import 'package:drift/internal/modular.dart' as i6;
|
||||||
|
|
||||||
typedef $$LocalAlbumEntityTableCreateCompanionBuilder =
|
typedef $$LocalAlbumEntityTableCreateCompanionBuilder =
|
||||||
i1.LocalAlbumEntityCompanion Function({
|
i1.LocalAlbumEntityCompanion Function({
|
||||||
|
|
@ -15,6 +18,7 @@ typedef $$LocalAlbumEntityTableCreateCompanionBuilder =
|
||||||
i0.Value<DateTime> updatedAt,
|
i0.Value<DateTime> updatedAt,
|
||||||
required i2.BackupSelection backupSelection,
|
required i2.BackupSelection backupSelection,
|
||||||
i0.Value<bool> isIosSharedAlbum,
|
i0.Value<bool> isIosSharedAlbum,
|
||||||
|
i0.Value<String?> linkedRemoteAlbumId,
|
||||||
i0.Value<bool?> marker_,
|
i0.Value<bool?> marker_,
|
||||||
});
|
});
|
||||||
typedef $$LocalAlbumEntityTableUpdateCompanionBuilder =
|
typedef $$LocalAlbumEntityTableUpdateCompanionBuilder =
|
||||||
|
|
@ -24,9 +28,57 @@ typedef $$LocalAlbumEntityTableUpdateCompanionBuilder =
|
||||||
i0.Value<DateTime> updatedAt,
|
i0.Value<DateTime> updatedAt,
|
||||||
i0.Value<i2.BackupSelection> backupSelection,
|
i0.Value<i2.BackupSelection> backupSelection,
|
||||||
i0.Value<bool> isIosSharedAlbum,
|
i0.Value<bool> isIosSharedAlbum,
|
||||||
|
i0.Value<String?> linkedRemoteAlbumId,
|
||||||
i0.Value<bool?> marker_,
|
i0.Value<bool?> marker_,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final class $$LocalAlbumEntityTableReferences
|
||||||
|
extends
|
||||||
|
i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$LocalAlbumEntityTable,
|
||||||
|
i1.LocalAlbumEntityData
|
||||||
|
> {
|
||||||
|
$$LocalAlbumEntityTableReferences(
|
||||||
|
super.$_db,
|
||||||
|
super.$_table,
|
||||||
|
super.$_typedResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
static i5.$RemoteAlbumEntityTable _linkedRemoteAlbumIdTable(
|
||||||
|
i0.GeneratedDatabase db,
|
||||||
|
) => i6.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity')
|
||||||
|
.createAlias(
|
||||||
|
i0.$_aliasNameGenerator(
|
||||||
|
i6.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$LocalAlbumEntityTable>('local_album_entity')
|
||||||
|
.linkedRemoteAlbumId,
|
||||||
|
i6.ReadDatabaseContainer(
|
||||||
|
db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity').id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
i5.$$RemoteAlbumEntityTableProcessedTableManager? get linkedRemoteAlbumId {
|
||||||
|
final $_column = $_itemColumn<String>('linked_remote_album_id');
|
||||||
|
if ($_column == null) return null;
|
||||||
|
final manager = i5
|
||||||
|
.$$RemoteAlbumEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i6.ReadDatabaseContainer(
|
||||||
|
$_db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||||
|
)
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_linkedRemoteAlbumIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class $$LocalAlbumEntityTableFilterComposer
|
class $$LocalAlbumEntityTableFilterComposer
|
||||||
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
|
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
|
||||||
$$LocalAlbumEntityTableFilterComposer({
|
$$LocalAlbumEntityTableFilterComposer({
|
||||||
|
|
@ -66,6 +118,33 @@ class $$LocalAlbumEntityTableFilterComposer
|
||||||
column: $table.marker_,
|
column: $table.marker_,
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i5.$$RemoteAlbumEntityTableFilterComposer get linkedRemoteAlbumId {
|
||||||
|
final i5.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
|
||||||
|
referencedTable: i6.ReadDatabaseContainer(
|
||||||
|
$db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder:
|
||||||
|
(
|
||||||
|
joinBuilder, {
|
||||||
|
$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
}) => i5.$$RemoteAlbumEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i6.ReadDatabaseContainer(
|
||||||
|
$db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAlbumEntityTableOrderingComposer
|
class $$LocalAlbumEntityTableOrderingComposer
|
||||||
|
|
@ -106,6 +185,34 @@ class $$LocalAlbumEntityTableOrderingComposer
|
||||||
column: $table.marker_,
|
column: $table.marker_,
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i5.$$RemoteAlbumEntityTableOrderingComposer get linkedRemoteAlbumId {
|
||||||
|
final i5.$$RemoteAlbumEntityTableOrderingComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
|
||||||
|
referencedTable: i6.ReadDatabaseContainer(
|
||||||
|
$db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder:
|
||||||
|
(
|
||||||
|
joinBuilder, {
|
||||||
|
$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
}) => i5.$$RemoteAlbumEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i6.ReadDatabaseContainer(
|
||||||
|
$db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAlbumEntityTableAnnotationComposer
|
class $$LocalAlbumEntityTableAnnotationComposer
|
||||||
|
|
@ -139,6 +246,34 @@ class $$LocalAlbumEntityTableAnnotationComposer
|
||||||
|
|
||||||
i0.GeneratedColumn<bool> get marker_ =>
|
i0.GeneratedColumn<bool> get marker_ =>
|
||||||
$composableBuilder(column: $table.marker_, builder: (column) => column);
|
$composableBuilder(column: $table.marker_, builder: (column) => column);
|
||||||
|
|
||||||
|
i5.$$RemoteAlbumEntityTableAnnotationComposer get linkedRemoteAlbumId {
|
||||||
|
final i5.$$RemoteAlbumEntityTableAnnotationComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
|
||||||
|
referencedTable: i6.ReadDatabaseContainer(
|
||||||
|
$db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder:
|
||||||
|
(
|
||||||
|
joinBuilder, {
|
||||||
|
$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
}) => i5.$$RemoteAlbumEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i6.ReadDatabaseContainer(
|
||||||
|
$db,
|
||||||
|
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAlbumEntityTableTableManager
|
class $$LocalAlbumEntityTableTableManager
|
||||||
|
|
@ -152,16 +287,9 @@ class $$LocalAlbumEntityTableTableManager
|
||||||
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
||||||
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
||||||
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
||||||
(
|
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
|
||||||
i1.LocalAlbumEntityData,
|
|
||||||
i0.BaseReferences<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$LocalAlbumEntityTable,
|
|
||||||
i1.LocalAlbumEntityData
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
i1.LocalAlbumEntityData,
|
i1.LocalAlbumEntityData,
|
||||||
i0.PrefetchHooks Function()
|
i0.PrefetchHooks Function({bool linkedRemoteAlbumId})
|
||||||
> {
|
> {
|
||||||
$$LocalAlbumEntityTableTableManager(
|
$$LocalAlbumEntityTableTableManager(
|
||||||
i0.GeneratedDatabase db,
|
i0.GeneratedDatabase db,
|
||||||
|
|
@ -187,6 +315,7 @@ class $$LocalAlbumEntityTableTableManager
|
||||||
i0.Value<i2.BackupSelection> backupSelection =
|
i0.Value<i2.BackupSelection> backupSelection =
|
||||||
const i0.Value.absent(),
|
const i0.Value.absent(),
|
||||||
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
|
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
|
||||||
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||||
}) => i1.LocalAlbumEntityCompanion(
|
}) => i1.LocalAlbumEntityCompanion(
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -194,6 +323,7 @@ class $$LocalAlbumEntityTableTableManager
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
backupSelection: backupSelection,
|
backupSelection: backupSelection,
|
||||||
isIosSharedAlbum: isIosSharedAlbum,
|
isIosSharedAlbum: isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||||
marker_: marker_,
|
marker_: marker_,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
|
|
@ -203,6 +333,7 @@ class $$LocalAlbumEntityTableTableManager
|
||||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
required i2.BackupSelection backupSelection,
|
required i2.BackupSelection backupSelection,
|
||||||
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
|
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
|
||||||
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||||
}) => i1.LocalAlbumEntityCompanion.insert(
|
}) => i1.LocalAlbumEntityCompanion.insert(
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -210,12 +341,60 @@ class $$LocalAlbumEntityTableTableManager
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
backupSelection: backupSelection,
|
backupSelection: backupSelection,
|
||||||
isIosSharedAlbum: isIosSharedAlbum,
|
isIosSharedAlbum: isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||||
marker_: marker_,
|
marker_: marker_,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
.map(
|
||||||
|
(e) => (
|
||||||
|
e.readTable(table),
|
||||||
|
i1.$$LocalAlbumEntityTableReferences(db, table, e),
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
prefetchHooksCallback: null,
|
prefetchHooksCallback: ({linkedRemoteAlbumId = false}) {
|
||||||
|
return i0.PrefetchHooks(
|
||||||
|
db: db,
|
||||||
|
explicitlyWatchedTables: [],
|
||||||
|
addJoins:
|
||||||
|
<
|
||||||
|
T extends i0.TableManagerState<
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic
|
||||||
|
>
|
||||||
|
>(state) {
|
||||||
|
if (linkedRemoteAlbumId) {
|
||||||
|
state =
|
||||||
|
state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.linkedRemoteAlbumId,
|
||||||
|
referencedTable: i1
|
||||||
|
.$$LocalAlbumEntityTableReferences
|
||||||
|
._linkedRemoteAlbumIdTable(db),
|
||||||
|
referencedColumn: i1
|
||||||
|
.$$LocalAlbumEntityTableReferences
|
||||||
|
._linkedRemoteAlbumIdTable(db)
|
||||||
|
.id,
|
||||||
|
)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
getPrefetchedDataCallback: (items) async {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -230,16 +409,9 @@ typedef $$LocalAlbumEntityTableProcessedTableManager =
|
||||||
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
||||||
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
||||||
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
||||||
(
|
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
|
||||||
i1.LocalAlbumEntityData,
|
|
||||||
i0.BaseReferences<
|
|
||||||
i0.GeneratedDatabase,
|
|
||||||
i1.$LocalAlbumEntityTable,
|
|
||||||
i1.LocalAlbumEntityData
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
i1.LocalAlbumEntityData,
|
i1.LocalAlbumEntityData,
|
||||||
i0.PrefetchHooks Function()
|
i0.PrefetchHooks Function({bool linkedRemoteAlbumId})
|
||||||
>;
|
>;
|
||||||
|
|
||||||
class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||||
|
|
@ -308,6 +480,20 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||||
),
|
),
|
||||||
defaultValue: const i4.Constant(false),
|
defaultValue: const i4.Constant(false),
|
||||||
);
|
);
|
||||||
|
static const i0.VerificationMeta _linkedRemoteAlbumIdMeta =
|
||||||
|
const i0.VerificationMeta('linkedRemoteAlbumId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> linkedRemoteAlbumId =
|
||||||
|
i0.GeneratedColumn<String>(
|
||||||
|
'linked_remote_album_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
|
||||||
|
),
|
||||||
|
);
|
||||||
static const i0.VerificationMeta _marker_Meta = const i0.VerificationMeta(
|
static const i0.VerificationMeta _marker_Meta = const i0.VerificationMeta(
|
||||||
'marker_',
|
'marker_',
|
||||||
);
|
);
|
||||||
|
|
@ -329,6 +515,7 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||||
updatedAt,
|
updatedAt,
|
||||||
backupSelection,
|
backupSelection,
|
||||||
isIosSharedAlbum,
|
isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId,
|
||||||
marker_,
|
marker_,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
|
|
@ -371,6 +558,15 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('linked_remote_album_id')) {
|
||||||
|
context.handle(
|
||||||
|
_linkedRemoteAlbumIdMeta,
|
||||||
|
linkedRemoteAlbumId.isAcceptableOrUnknown(
|
||||||
|
data['linked_remote_album_id']!,
|
||||||
|
_linkedRemoteAlbumIdMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('marker')) {
|
if (data.containsKey('marker')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_marker_Meta,
|
_marker_Meta,
|
||||||
|
|
@ -412,6 +608,10 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||||
i0.DriftSqlType.bool,
|
i0.DriftSqlType.bool,
|
||||||
data['${effectivePrefix}is_ios_shared_album'],
|
data['${effectivePrefix}is_ios_shared_album'],
|
||||||
)!,
|
)!,
|
||||||
|
linkedRemoteAlbumId: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}linked_remote_album_id'],
|
||||||
|
),
|
||||||
marker_: attachedDatabase.typeMapping.read(
|
marker_: attachedDatabase.typeMapping.read(
|
||||||
i0.DriftSqlType.bool,
|
i0.DriftSqlType.bool,
|
||||||
data['${effectivePrefix}marker'],
|
data['${effectivePrefix}marker'],
|
||||||
|
|
@ -441,6 +641,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final i2.BackupSelection backupSelection;
|
final i2.BackupSelection backupSelection;
|
||||||
final bool isIosSharedAlbum;
|
final bool isIosSharedAlbum;
|
||||||
|
final String? linkedRemoteAlbumId;
|
||||||
final bool? marker_;
|
final bool? marker_;
|
||||||
const LocalAlbumEntityData({
|
const LocalAlbumEntityData({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -448,6 +649,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.backupSelection,
|
required this.backupSelection,
|
||||||
required this.isIosSharedAlbum,
|
required this.isIosSharedAlbum,
|
||||||
|
this.linkedRemoteAlbumId,
|
||||||
this.marker_,
|
this.marker_,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
|
|
@ -464,6 +666,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum);
|
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum);
|
||||||
|
if (!nullToAbsent || linkedRemoteAlbumId != null) {
|
||||||
|
map['linked_remote_album_id'] = i0.Variable<String>(linkedRemoteAlbumId);
|
||||||
|
}
|
||||||
if (!nullToAbsent || marker_ != null) {
|
if (!nullToAbsent || marker_ != null) {
|
||||||
map['marker'] = i0.Variable<bool>(marker_);
|
map['marker'] = i0.Variable<bool>(marker_);
|
||||||
}
|
}
|
||||||
|
|
@ -482,6 +687,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
|
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
|
||||||
.fromJson(serializer.fromJson<int>(json['backupSelection'])),
|
.fromJson(serializer.fromJson<int>(json['backupSelection'])),
|
||||||
isIosSharedAlbum: serializer.fromJson<bool>(json['isIosSharedAlbum']),
|
isIosSharedAlbum: serializer.fromJson<bool>(json['isIosSharedAlbum']),
|
||||||
|
linkedRemoteAlbumId: serializer.fromJson<String?>(
|
||||||
|
json['linkedRemoteAlbumId'],
|
||||||
|
),
|
||||||
marker_: serializer.fromJson<bool?>(json['marker_']),
|
marker_: serializer.fromJson<bool?>(json['marker_']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -498,6 +706,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
'isIosSharedAlbum': serializer.toJson<bool>(isIosSharedAlbum),
|
'isIosSharedAlbum': serializer.toJson<bool>(isIosSharedAlbum),
|
||||||
|
'linkedRemoteAlbumId': serializer.toJson<String?>(linkedRemoteAlbumId),
|
||||||
'marker_': serializer.toJson<bool?>(marker_),
|
'marker_': serializer.toJson<bool?>(marker_),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -508,6 +717,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
i2.BackupSelection? backupSelection,
|
i2.BackupSelection? backupSelection,
|
||||||
bool? isIosSharedAlbum,
|
bool? isIosSharedAlbum,
|
||||||
|
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
|
||||||
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||||
}) => i1.LocalAlbumEntityData(
|
}) => i1.LocalAlbumEntityData(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -515,6 +725,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
backupSelection: backupSelection ?? this.backupSelection,
|
backupSelection: backupSelection ?? this.backupSelection,
|
||||||
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId.present
|
||||||
|
? linkedRemoteAlbumId.value
|
||||||
|
: this.linkedRemoteAlbumId,
|
||||||
marker_: marker_.present ? marker_.value : this.marker_,
|
marker_: marker_.present ? marker_.value : this.marker_,
|
||||||
);
|
);
|
||||||
LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) {
|
LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) {
|
||||||
|
|
@ -528,6 +741,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
isIosSharedAlbum: data.isIosSharedAlbum.present
|
isIosSharedAlbum: data.isIosSharedAlbum.present
|
||||||
? data.isIosSharedAlbum.value
|
? data.isIosSharedAlbum.value
|
||||||
: this.isIosSharedAlbum,
|
: this.isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId: data.linkedRemoteAlbumId.present
|
||||||
|
? data.linkedRemoteAlbumId.value
|
||||||
|
: this.linkedRemoteAlbumId,
|
||||||
marker_: data.marker_.present ? data.marker_.value : this.marker_,
|
marker_: data.marker_.present ? data.marker_.value : this.marker_,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -540,6 +756,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
..write('updatedAt: $updatedAt, ')
|
..write('updatedAt: $updatedAt, ')
|
||||||
..write('backupSelection: $backupSelection, ')
|
..write('backupSelection: $backupSelection, ')
|
||||||
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
|
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
|
||||||
|
..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ')
|
||||||
..write('marker_: $marker_')
|
..write('marker_: $marker_')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
|
|
@ -552,6 +769,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
updatedAt,
|
updatedAt,
|
||||||
backupSelection,
|
backupSelection,
|
||||||
isIosSharedAlbum,
|
isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId,
|
||||||
marker_,
|
marker_,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
|
|
@ -563,6 +781,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||||
other.updatedAt == this.updatedAt &&
|
other.updatedAt == this.updatedAt &&
|
||||||
other.backupSelection == this.backupSelection &&
|
other.backupSelection == this.backupSelection &&
|
||||||
other.isIosSharedAlbum == this.isIosSharedAlbum &&
|
other.isIosSharedAlbum == this.isIosSharedAlbum &&
|
||||||
|
other.linkedRemoteAlbumId == this.linkedRemoteAlbumId &&
|
||||||
other.marker_ == this.marker_);
|
other.marker_ == this.marker_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -573,6 +792,7 @@ class LocalAlbumEntityCompanion
|
||||||
final i0.Value<DateTime> updatedAt;
|
final i0.Value<DateTime> updatedAt;
|
||||||
final i0.Value<i2.BackupSelection> backupSelection;
|
final i0.Value<i2.BackupSelection> backupSelection;
|
||||||
final i0.Value<bool> isIosSharedAlbum;
|
final i0.Value<bool> isIosSharedAlbum;
|
||||||
|
final i0.Value<String?> linkedRemoteAlbumId;
|
||||||
final i0.Value<bool?> marker_;
|
final i0.Value<bool?> marker_;
|
||||||
const LocalAlbumEntityCompanion({
|
const LocalAlbumEntityCompanion({
|
||||||
this.id = const i0.Value.absent(),
|
this.id = const i0.Value.absent(),
|
||||||
|
|
@ -580,6 +800,7 @@ class LocalAlbumEntityCompanion
|
||||||
this.updatedAt = const i0.Value.absent(),
|
this.updatedAt = const i0.Value.absent(),
|
||||||
this.backupSelection = const i0.Value.absent(),
|
this.backupSelection = const i0.Value.absent(),
|
||||||
this.isIosSharedAlbum = const i0.Value.absent(),
|
this.isIosSharedAlbum = const i0.Value.absent(),
|
||||||
|
this.linkedRemoteAlbumId = const i0.Value.absent(),
|
||||||
this.marker_ = const i0.Value.absent(),
|
this.marker_ = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
LocalAlbumEntityCompanion.insert({
|
LocalAlbumEntityCompanion.insert({
|
||||||
|
|
@ -588,6 +809,7 @@ class LocalAlbumEntityCompanion
|
||||||
this.updatedAt = const i0.Value.absent(),
|
this.updatedAt = const i0.Value.absent(),
|
||||||
required i2.BackupSelection backupSelection,
|
required i2.BackupSelection backupSelection,
|
||||||
this.isIosSharedAlbum = const i0.Value.absent(),
|
this.isIosSharedAlbum = const i0.Value.absent(),
|
||||||
|
this.linkedRemoteAlbumId = const i0.Value.absent(),
|
||||||
this.marker_ = const i0.Value.absent(),
|
this.marker_ = const i0.Value.absent(),
|
||||||
}) : id = i0.Value(id),
|
}) : id = i0.Value(id),
|
||||||
name = i0.Value(name),
|
name = i0.Value(name),
|
||||||
|
|
@ -598,6 +820,7 @@ class LocalAlbumEntityCompanion
|
||||||
i0.Expression<DateTime>? updatedAt,
|
i0.Expression<DateTime>? updatedAt,
|
||||||
i0.Expression<int>? backupSelection,
|
i0.Expression<int>? backupSelection,
|
||||||
i0.Expression<bool>? isIosSharedAlbum,
|
i0.Expression<bool>? isIosSharedAlbum,
|
||||||
|
i0.Expression<String>? linkedRemoteAlbumId,
|
||||||
i0.Expression<bool>? marker_,
|
i0.Expression<bool>? marker_,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
|
|
@ -606,6 +829,8 @@ class LocalAlbumEntityCompanion
|
||||||
if (updatedAt != null) 'updated_at': updatedAt,
|
if (updatedAt != null) 'updated_at': updatedAt,
|
||||||
if (backupSelection != null) 'backup_selection': backupSelection,
|
if (backupSelection != null) 'backup_selection': backupSelection,
|
||||||
if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum,
|
if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum,
|
||||||
|
if (linkedRemoteAlbumId != null)
|
||||||
|
'linked_remote_album_id': linkedRemoteAlbumId,
|
||||||
if (marker_ != null) 'marker': marker_,
|
if (marker_ != null) 'marker': marker_,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -616,6 +841,7 @@ class LocalAlbumEntityCompanion
|
||||||
i0.Value<DateTime>? updatedAt,
|
i0.Value<DateTime>? updatedAt,
|
||||||
i0.Value<i2.BackupSelection>? backupSelection,
|
i0.Value<i2.BackupSelection>? backupSelection,
|
||||||
i0.Value<bool>? isIosSharedAlbum,
|
i0.Value<bool>? isIosSharedAlbum,
|
||||||
|
i0.Value<String?>? linkedRemoteAlbumId,
|
||||||
i0.Value<bool?>? marker_,
|
i0.Value<bool?>? marker_,
|
||||||
}) {
|
}) {
|
||||||
return i1.LocalAlbumEntityCompanion(
|
return i1.LocalAlbumEntityCompanion(
|
||||||
|
|
@ -624,6 +850,7 @@ class LocalAlbumEntityCompanion
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
backupSelection: backupSelection ?? this.backupSelection,
|
backupSelection: backupSelection ?? this.backupSelection,
|
||||||
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId,
|
||||||
marker_: marker_ ?? this.marker_,
|
marker_: marker_ ?? this.marker_,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -650,6 +877,11 @@ class LocalAlbumEntityCompanion
|
||||||
if (isIosSharedAlbum.present) {
|
if (isIosSharedAlbum.present) {
|
||||||
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum.value);
|
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum.value);
|
||||||
}
|
}
|
||||||
|
if (linkedRemoteAlbumId.present) {
|
||||||
|
map['linked_remote_album_id'] = i0.Variable<String>(
|
||||||
|
linkedRemoteAlbumId.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (marker_.present) {
|
if (marker_.present) {
|
||||||
map['marker'] = i0.Variable<bool>(marker_.value);
|
map['marker'] = i0.Variable<bool>(marker_.value);
|
||||||
}
|
}
|
||||||
|
|
@ -664,6 +896,7 @@ class LocalAlbumEntityCompanion
|
||||||
..write('updatedAt: $updatedAt, ')
|
..write('updatedAt: $updatedAt, ')
|
||||||
..write('backupSelection: $backupSelection, ')
|
..write('backupSelection: $backupSelection, ')
|
||||||
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
|
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
|
||||||
|
..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ')
|
||||||
..write('marker_: $marker_')
|
..write('marker_: $marker_')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
|
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||||
LocalAsset toDto() => LocalAsset(
|
LocalAsset toDto() => LocalAsset(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ import 'package:drift/drift.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import "package:immich_mobile/utils/database.utils.dart";
|
|
||||||
|
|
||||||
final backupRepositoryProvider = Provider<DriftBackupRepository>(
|
final backupRepositoryProvider = Provider<DriftBackupRepository>(
|
||||||
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 8;
|
int get schemaVersion => 9;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
|
@ -123,6 +123,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||||
from7To8: (m, v8) async {
|
from7To8: (m, v8) async {
|
||||||
await m.create(v8.storeEntity);
|
await m.create(v8.storeEntity);
|
||||||
},
|
},
|
||||||
|
from8To9: (m, v9) async {
|
||||||
|
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,17 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||||
as i3;
|
as i3;
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||||
as i4;
|
as i4;
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
|
||||||
as i5;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
|
||||||
as i6;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
|
||||||
as i7;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
|
||||||
as i8;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
|
||||||
as i9;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||||
|
as i5;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||||
|
as i6;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||||
|
as i7;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||||
|
as i8;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||||
|
as i9;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||||
as i10;
|
as i10;
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||||
as i11;
|
as i11;
|
||||||
|
|
@ -48,19 +48,19 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this);
|
late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this);
|
||||||
late final i4.$LocalAssetEntityTable localAssetEntity = i4
|
late final i4.$LocalAssetEntityTable localAssetEntity = i4
|
||||||
.$LocalAssetEntityTable(this);
|
.$LocalAssetEntityTable(this);
|
||||||
late final i5.$LocalAlbumEntityTable localAlbumEntity = i5
|
late final i5.$RemoteAlbumEntityTable remoteAlbumEntity = i5
|
||||||
|
.$RemoteAlbumEntityTable(this);
|
||||||
|
late final i6.$LocalAlbumEntityTable localAlbumEntity = i6
|
||||||
.$LocalAlbumEntityTable(this);
|
.$LocalAlbumEntityTable(this);
|
||||||
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i6
|
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7
|
||||||
.$LocalAlbumAssetEntityTable(this);
|
.$LocalAlbumAssetEntityTable(this);
|
||||||
late final i7.$UserMetadataEntityTable userMetadataEntity = i7
|
late final i8.$UserMetadataEntityTable userMetadataEntity = i8
|
||||||
.$UserMetadataEntityTable(this);
|
.$UserMetadataEntityTable(this);
|
||||||
late final i8.$PartnerEntityTable partnerEntity = i8.$PartnerEntityTable(
|
late final i9.$PartnerEntityTable partnerEntity = i9.$PartnerEntityTable(
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
late final i9.$RemoteExifEntityTable remoteExifEntity = i9
|
late final i10.$RemoteExifEntityTable remoteExifEntity = i10
|
||||||
.$RemoteExifEntityTable(this);
|
.$RemoteExifEntityTable(this);
|
||||||
late final i10.$RemoteAlbumEntityTable remoteAlbumEntity = i10
|
|
||||||
.$RemoteAlbumEntityTable(this);
|
|
||||||
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i11
|
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i11
|
||||||
.$RemoteAlbumAssetEntityTable(this);
|
.$RemoteAlbumAssetEntityTable(this);
|
||||||
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i12
|
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i12
|
||||||
|
|
@ -84,6 +84,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
remoteAssetEntity,
|
remoteAssetEntity,
|
||||||
stackEntity,
|
stackEntity,
|
||||||
localAssetEntity,
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
localAlbumEntity,
|
localAlbumEntity,
|
||||||
localAlbumAssetEntity,
|
localAlbumAssetEntity,
|
||||||
i4.idxLocalAssetChecksum,
|
i4.idxLocalAssetChecksum,
|
||||||
|
|
@ -94,7 +95,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
userMetadataEntity,
|
userMetadataEntity,
|
||||||
partnerEntity,
|
partnerEntity,
|
||||||
remoteExifEntity,
|
remoteExifEntity,
|
||||||
remoteAlbumEntity,
|
|
||||||
remoteAlbumAssetEntity,
|
remoteAlbumAssetEntity,
|
||||||
remoteAlbumUserEntity,
|
remoteAlbumUserEntity,
|
||||||
memoryEntity,
|
memoryEntity,
|
||||||
|
|
@ -102,7 +102,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
personEntity,
|
personEntity,
|
||||||
assetFaceEntity,
|
assetFaceEntity,
|
||||||
storeEntity,
|
storeEntity,
|
||||||
i9.idxLatLng,
|
i10.idxLatLng,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
i0.StreamQueryUpdateRules
|
i0.StreamQueryUpdateRules
|
||||||
|
|
@ -123,6 +123,33 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
),
|
),
|
||||||
result: [i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete)],
|
result: [i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete)],
|
||||||
),
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName(
|
||||||
|
'user_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete,
|
||||||
|
),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName(
|
||||||
|
'remote_asset_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete,
|
||||||
|
),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName(
|
||||||
|
'remote_album_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete,
|
||||||
|
),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('local_album_entity', kind: i0.UpdateKind.update),
|
||||||
|
],
|
||||||
|
),
|
||||||
i0.WritePropagation(
|
i0.WritePropagation(
|
||||||
on: i0.TableUpdateQuery.onTableName(
|
on: i0.TableUpdateQuery.onTableName(
|
||||||
'local_asset_entity',
|
'local_asset_entity',
|
||||||
|
|
@ -173,24 +200,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
|
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName(
|
|
||||||
'user_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete,
|
|
||||||
),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
|
||||||
on: i0.TableUpdateQuery.onTableName(
|
|
||||||
'remote_asset_entity',
|
|
||||||
limitUpdateKind: i0.UpdateKind.delete,
|
|
||||||
),
|
|
||||||
result: [
|
|
||||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
i0.WritePropagation(
|
i0.WritePropagation(
|
||||||
on: i0.TableUpdateQuery.onTableName(
|
on: i0.TableUpdateQuery.onTableName(
|
||||||
'remote_asset_entity',
|
'remote_asset_entity',
|
||||||
|
|
@ -290,18 +299,18 @@ class $DriftManager {
|
||||||
i3.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
i3.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||||
i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||||
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||||
i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
i5.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||||
i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
i5.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||||
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
|
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||||
|
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||||
|
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||||
i7.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
i8.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||||
i7.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
i8.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||||
i8.$$PartnerEntityTableTableManager get partnerEntity =>
|
i9.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||||
i8.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
i9.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||||
i9.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
i10.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||||
i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
i10.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||||
i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
|
||||||
i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
|
||||||
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||||
i11.$$RemoteAlbumAssetEntityTableTableManager(
|
i11.$$RemoteAlbumAssetEntityTableTableManager(
|
||||||
_db,
|
_db,
|
||||||
|
|
|
||||||
|
|
@ -3435,6 +3435,391 @@ i1.GeneratedColumn<int> _column_89(String aliasedName) =>
|
||||||
true,
|
true,
|
||||||
type: i1.DriftSqlType.int,
|
type: i1.DriftSqlType.int,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema9 extends i0.VersionedSchema {
|
||||||
|
Schema9({required super.database}) : super(version: 9);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxRemoteAssetOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
idxLatLng,
|
||||||
|
];
|
||||||
|
late final Shape16 userEntity = Shape16(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_5,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape17 remoteAssetEntity = Shape17(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_86,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 stackEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape2 localAssetEntity = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape9 remoteAlbumEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_56,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape19 localAlbumEntity = Shape19(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_90,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape7 localAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_34, _column_35],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
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 idxRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_owner_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||||
|
);
|
||||||
|
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)',
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_25, _column_26, _column_27],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape5 partnerEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_28, _column_29, _column_30],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape8 remoteExifEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
_column_54,
|
||||||
|
_column_55,
|
||||||
|
],
|
||||||
|
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_36, _column_60],
|
||||||
|
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_60, _column_25, _column_61],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 memoryEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
_column_66,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
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_36, _column_68],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape14 personEntity = Shape14(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_1,
|
||||||
|
_column_69,
|
||||||
|
_column_71,
|
||||||
|
_column_72,
|
||||||
|
_column_73,
|
||||||
|
_column_74,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape15 assetFaceEntity = Shape15(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_36,
|
||||||
|
_column_76,
|
||||||
|
_column_77,
|
||||||
|
_column_78,
|
||||||
|
_column_79,
|
||||||
|
_column_80,
|
||||||
|
_column_81,
|
||||||
|
_column_82,
|
||||||
|
_column_83,
|
||||||
|
],
|
||||||
|
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_87, _column_88, _column_89],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape19 extends i0.VersionedTable {
|
||||||
|
Shape19({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get backupSelection =>
|
||||||
|
columnsByName['backup_selection']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isIosSharedAlbum =>
|
||||||
|
columnsByName['is_ios_shared_album']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get linkedRemoteAlbumId =>
|
||||||
|
columnsByName['linked_remote_album_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get marker_ =>
|
||||||
|
columnsByName['marker']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_90(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'linked_remote_album_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
|
||||||
|
),
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
|
|
@ -3443,6 +3828,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
|
|
@ -3481,6 +3867,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from7To8(migrator, schema);
|
await from7To8(migrator, schema);
|
||||||
return 8;
|
return 8;
|
||||||
|
case 8:
|
||||||
|
final schema = Schema9(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from8To9(migrator, schema);
|
||||||
|
return 9;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
|
|
@ -3495,6 +3886,7 @@ i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
|
|
@ -3504,5 +3896,6 @@ i1.OnUpgrade stepByStep({
|
||||||
from5To6: from5To6,
|
from5To6: from5To6,
|
||||||
from6To7: from6To7,
|
from6To7: from6To7,
|
||||||
from7To8: from7To8,
|
from7To8: from7To8,
|
||||||
|
from8To9: from8To9,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||||
|
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/local_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/utils/database.utils.dart';
|
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
||||||
|
|
@ -49,6 +50,13 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||||
return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get();
|
return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<LocalAlbum>> getBackupAlbums() async {
|
||||||
|
final query = _db.localAlbumEntity.select()
|
||||||
|
..where((row) => row.backupSelection.equalsValue(BackupSelection.selected));
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> delete(String albumId) => transaction(() async {
|
Future<void> delete(String albumId) => transaction(() async {
|
||||||
// Remove all assets that are only in this particular album
|
// Remove all assets that are only in this particular album
|
||||||
// We cannot remove all assets in the album because they might be in other albums in iOS
|
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||||
|
|
@ -335,4 +343,16 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _db.managers.localAlbumEntity.count();
|
return _db.managers.localAlbumEntity.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future unlinkRemoteAlbum(String id) async {
|
||||||
|
return _db.localAlbumEntity.update()
|
||||||
|
..where((row) => row.id.equals(id))
|
||||||
|
..write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
|
||||||
|
return _db.localAlbumEntity.update()
|
||||||
|
..where((row) => row.id.equals(localAlbumId))
|
||||||
|
..write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||||
|
final query = _db.remoteAlbumEntity.select()
|
||||||
|
..where((row) => row.name.equals(albumName) & row.ownerId.equals(ownerId))
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(1);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto(ownerName: '', isShared: false)).getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
|
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
|
||||||
await _db.transaction(() async {
|
await _db.transaction(() async {
|
||||||
final entity = RemoteAlbumEntityCompanion(
|
final entity = RemoteAlbumEntityCompanion(
|
||||||
|
|
@ -321,6 +330,42 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _db.managers.remoteAlbumEntity.count();
|
return _db.managers.remoteAlbumEntity.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getLinkedAssetIds(String userId, String localAlbumId, String remoteAlbumId) async {
|
||||||
|
// Find remote asset ids that:
|
||||||
|
// 1. Belong to the provided local album (via local_album_asset_entity)
|
||||||
|
// 2. Have been uploaded (i.e. a matching remote asset exists for the same checksum & owner)
|
||||||
|
// 3. Are NOT already in the remote album (remote_album_asset_entity)
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.remoteAssetEntity.id])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
// Left join remote album assets to exclude those already in the remote album
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id) &
|
||||||
|
_db.remoteAlbumAssetEntity.albumId.equals(remoteAlbumId),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
|
_db.localAlbumAssetEntity.albumId.equals(localAlbumId) &
|
||||||
|
_db.remoteAlbumAssetEntity.assetId.isNull(), // only those not yet linked
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
|
@ -26,7 +25,6 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
|
|
@ -207,12 +205,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||||
// needs to be delayed so that EasyLocalization is working
|
// needs to be delayed so that EasyLocalization is working
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
ref.read(backgroundServiceProvider).disableService();
|
ref.read(backgroundServiceProvider).disableService();
|
||||||
ref.read(driftBackgroundUploadFgService).enableSyncService();
|
ref.read(driftBackgroundUploadFgService).enable();
|
||||||
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
|
|
||||||
ref.read(driftBackgroundUploadFgService).enableUploadService();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ref.read(driftBackgroundUploadFgService).disableUploadService();
|
ref.read(driftBackgroundUploadFgService).disable();
|
||||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.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/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
@ -43,12 +42,10 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
|
|
||||||
await ref.read(backgroundSyncProvider).syncRemote();
|
await ref.read(backgroundSyncProvider).syncRemote();
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||||
await ref.read(driftBackgroundUploadFgService).enableUploadService();
|
|
||||||
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stopBackup() async {
|
Future<void> stopBackup() async {
|
||||||
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
|
||||||
await ref.read(driftBackupProvider.notifier).cancel();
|
await ref.read(driftBackupProvider.notifier).cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.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/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
@ -26,10 +27,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
bool _isSearchMode = false;
|
bool _isSearchMode = false;
|
||||||
int _initialTotalAssetCount = 0;
|
int _initialTotalAssetCount = 0;
|
||||||
bool _hasPopped = false;
|
|
||||||
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
||||||
late TextEditingController _searchController;
|
late TextEditingController _searchController;
|
||||||
late FocusNode _searchFocusNode;
|
late FocusNode _searchFocusNode;
|
||||||
|
Future? _handleLinkedAlbumFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -44,6 +45,36 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePagePopped() async {
|
||||||
|
final user = ref.read(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
final selectedAlbums = ref
|
||||||
|
.read(backupAlbumProvider)
|
||||||
|
.where((a) => a.backupSelection == BackupSelection.selected)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (enableSyncUploadAlbum && selectedAlbums.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_handleLinkedAlbumFuture = ref.read(syncLinkedAlbumServiceProvider).manageLinkedAlbums(selectedAlbums, user.id);
|
||||||
|
});
|
||||||
|
await _handleLinkedAlbumFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart backup if total count changed and backup is enabled
|
||||||
|
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||||
|
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
|
||||||
|
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
|
if (totalChanged && isBackupEnabled) {
|
||||||
|
await ref.read(driftBackupProvider.notifier).cancel();
|
||||||
|
await ref.read(driftBackupProvider.notifier).startBackup(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_enableSyncUploadAlbum.dispose();
|
_enableSyncUploadAlbum.dispose();
|
||||||
|
|
@ -65,42 +96,12 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
||||||
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
||||||
|
|
||||||
// handleSyncAlbumToggle(bool isEnable) async {
|
|
||||||
// if (isEnable) {
|
|
||||||
// await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
|
||||||
// for (final album in selectedBackupAlbums) {
|
|
||||||
// await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
canPop: false,
|
||||||
// There is an issue with Flutter where the pop event
|
onPopInvokedWithResult: (didPop, _) async {
|
||||||
// can be triggered multiple times, so we guard it with _hasPopped
|
if (!didPop) {
|
||||||
if (didPop && !_hasPopped) {
|
await _handlePagePopped();
|
||||||
_hasPopped = true;
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
final currentUser = ref.read(currentUserProvider);
|
|
||||||
if (currentUser == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
|
||||||
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
|
||||||
|
|
||||||
if (currentTotalAssetCount != _initialTotalAssetCount) {
|
|
||||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
|
||||||
|
|
||||||
if (!isBackupEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
|
||||||
|
|
||||||
backupNotifier.cancel().then((_) {
|
|
||||||
backupNotifier.startBackup(currentUser.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
|
@ -139,103 +140,123 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
],
|
],
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
body: Stack(
|
||||||
physics: const ClampingScrollPhysics(),
|
children: [
|
||||||
slivers: [
|
CustomScrollView(
|
||||||
SliverToBoxAdapter(
|
physics: const ClampingScrollPhysics(),
|
||||||
child: Column(
|
slivers: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
SliverToBoxAdapter(
|
||||||
children: [
|
child: Column(
|
||||||
Padding(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
children: [
|
||||||
child: Text(
|
Padding(
|
||||||
"backup_album_selection_page_selection_info",
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
style: context.textTheme.titleSmall,
|
child: Text(
|
||||||
).t(context: context),
|
"backup_album_selection_page_selection_info",
|
||||||
),
|
style: context.textTheme.titleSmall,
|
||||||
|
).t(context: context),
|
||||||
|
),
|
||||||
|
|
||||||
// Selected Album Chips
|
// Selected Album Chips
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: [
|
children: [
|
||||||
_SelectedAlbumNameChips(selectedBackupAlbums: selectedBackupAlbums),
|
_SelectedAlbumNameChips(selectedBackupAlbums: selectedBackupAlbums),
|
||||||
_ExcludedAlbumNameChips(excludedBackupAlbums: excludedBackupAlbums),
|
_ExcludedAlbumNameChips(excludedBackupAlbums: excludedBackupAlbums),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
// SettingsSwitchListTile(
|
title: Text(
|
||||||
// valueNotifier: _enableSyncUploadAlbum,
|
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
||||||
// title: "sync_albums".t(context: context),
|
style: context.textTheme.titleSmall,
|
||||||
// subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
),
|
||||||
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
subtitle: Padding(
|
||||||
// titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
// subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
child: Text(
|
||||||
// onChanged: handleSyncAlbumToggle,
|
"backup_album_selection_page_albums_tap",
|
||||||
// ),
|
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
|
||||||
ListTile(
|
).t(context: context),
|
||||||
title: Text(
|
),
|
||||||
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
trailing: IconButton(
|
||||||
style: context.textTheme.titleSmall,
|
splashRadius: 16,
|
||||||
),
|
icon: Icon(Icons.info, size: 20, color: context.primaryColor),
|
||||||
subtitle: Padding(
|
onPressed: () {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
showDialog(
|
||||||
child: Text(
|
context: context,
|
||||||
"backup_album_selection_page_albums_tap",
|
builder: (BuildContext context) {
|
||||||
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
|
return AlertDialog(
|
||||||
).t(context: context),
|
shape: const RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
trailing: IconButton(
|
),
|
||||||
splashRadius: 16,
|
elevation: 5,
|
||||||
icon: Icon(Icons.info, size: 20, color: context.primaryColor),
|
title: Text(
|
||||||
onPressed: () {
|
'backup_album_selection_page_selection_info',
|
||||||
// show the dialog
|
style: TextStyle(
|
||||||
showDialog(
|
fontSize: 16,
|
||||||
context: context,
|
fontWeight: FontWeight.bold,
|
||||||
builder: (BuildContext context) {
|
color: context.primaryColor,
|
||||||
return AlertDialog(
|
),
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
).t(context: context),
|
||||||
elevation: 5,
|
content: SingleChildScrollView(
|
||||||
title: Text(
|
child: ListBody(
|
||||||
'backup_album_selection_page_selection_info',
|
children: [
|
||||||
style: TextStyle(
|
const Text(
|
||||||
fontSize: 16,
|
'backup_album_selection_page_assets_scatter',
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(fontSize: 14),
|
||||||
color: context.primaryColor,
|
).t(context: context),
|
||||||
),
|
],
|
||||||
).t(context: context),
|
),
|
||||||
content: SingleChildScrollView(
|
),
|
||||||
child: ListBody(
|
);
|
||||||
children: [
|
},
|
||||||
const Text(
|
|
||||||
'backup_album_selection_page_assets_scatter',
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
).t(context: context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (Platform.isAndroid)
|
if (Platform.isAndroid)
|
||||||
_SelectAllButton(filteredAlbums: filteredAlbums, selectedBackupAlbums: selectedBackupAlbums),
|
_SelectAllButton(filteredAlbums: filteredAlbums, selectedBackupAlbums: selectedBackupAlbums),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverLayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
if (constraints.crossAxisExtent > 600) {
|
||||||
|
return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||||
|
} else {
|
||||||
|
return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_handleLinkedAlbumFuture != null)
|
||||||
|
FutureBuilder(
|
||||||
|
future: _handleLinkedAlbumFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Container(
|
||||||
|
color: context.scaffoldBackgroundColor.withValues(alpha: 0.8),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
spacing: 16,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(strokeWidth: 4),
|
||||||
|
Text("Creating linked albums...", style: context.textTheme.labelLarge),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
SliverLayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
if (constraints.crossAxisExtent > 600) {
|
|
||||||
return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
|
||||||
} else {
|
|
||||||
return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
await ref.read(driftBackgroundUploadFgService).disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
|
@ -21,14 +23,23 @@ class SplashScreenPage extends StatefulHookConsumerWidget {
|
||||||
|
|
||||||
class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||||
final log = Logger("SplashScreenPage");
|
final log = Logger("SplashScreenPage");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
ref
|
final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
||||||
.read(authProvider.notifier)
|
|
||||||
.setOpenApiServiceEndpoint()
|
lockManager.requestHolderToClose();
|
||||||
.then(logConnectionInfo)
|
lockManager
|
||||||
.whenComplete(() => resumeSession());
|
.acquireLock()
|
||||||
|
.timeout(const Duration(seconds: 5))
|
||||||
|
.whenComplete(
|
||||||
|
() => ref
|
||||||
|
.read(authProvider.notifier)
|
||||||
|
.setOpenApiServiceEndpoint()
|
||||||
|
.then(logConnectionInfo)
|
||||||
|
.whenComplete(() => resumeSession()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void logConnectionInfo(String? endpoint) {
|
void logConnectionInfo(String? endpoint) {
|
||||||
|
|
|
||||||
62
mobile/lib/platform/background_worker_api.g.dart
generated
62
mobile/lib/platform/background_worker_api.g.dart
generated
|
|
@ -59,9 +59,9 @@ class BackgroundWorkerFgHostApi {
|
||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
Future<void> enableSyncWorker() async {
|
Future<void> enable() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
|
|
@ -82,32 +82,9 @@ class BackgroundWorkerFgHostApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> enableUploadWorker() async {
|
Future<void> disable() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
|
||||||
pigeonChannelCodec,
|
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
|
||||||
);
|
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
|
||||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
|
||||||
if (pigeonVar_replyList == null) {
|
|
||||||
throw _createConnectionError(pigeonVar_channelName);
|
|
||||||
} else if (pigeonVar_replyList.length > 1) {
|
|
||||||
throw PlatformException(
|
|
||||||
code: pigeonVar_replyList[0]! as String,
|
|
||||||
message: pigeonVar_replyList[1] as String?,
|
|
||||||
details: pigeonVar_replyList[2],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> disableUploadWorker() async {
|
|
||||||
final String pigeonVar_channelName =
|
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$pigeonVar_messageChannelSuffix';
|
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
|
|
@ -192,8 +169,6 @@ class BackgroundWorkerBgHostApi {
|
||||||
abstract class BackgroundWorkerFlutterApi {
|
abstract class BackgroundWorkerFlutterApi {
|
||||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
Future<void> onLocalSync(int? maxSeconds);
|
|
||||||
|
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|
||||||
Future<void> onAndroidUpload();
|
Future<void> onAndroidUpload();
|
||||||
|
|
@ -206,35 +181,6 @@ abstract class BackgroundWorkerFlutterApi {
|
||||||
String messageChannelSuffix = '',
|
String messageChannelSuffix = '',
|
||||||
}) {
|
}) {
|
||||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
{
|
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$messageChannelSuffix',
|
|
||||||
pigeonChannelCodec,
|
|
||||||
binaryMessenger: binaryMessenger,
|
|
||||||
);
|
|
||||||
if (api == null) {
|
|
||||||
pigeonVar_channel.setMessageHandler(null);
|
|
||||||
} else {
|
|
||||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
|
||||||
assert(
|
|
||||||
message != null,
|
|
||||||
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync was null.',
|
|
||||||
);
|
|
||||||
final List<Object?> args = (message as List<Object?>?)!;
|
|
||||||
final int? arg_maxSeconds = (args[0] as int?);
|
|
||||||
try {
|
|
||||||
await api.onLocalSync(arg_maxSeconds);
|
|
||||||
return wrapResponse(empty: true);
|
|
||||||
} on PlatformException catch (e) {
|
|
||||||
return wrapResponse(error: e);
|
|
||||||
} catch (e) {
|
|
||||||
return wrapResponse(
|
|
||||||
error: PlatformException(code: 'error', message: e.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
|
@ -38,14 +39,14 @@ class DriftPlacePage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlaceSliverAppBar extends StatelessWidget {
|
class _PlaceSliverAppBar extends HookWidget {
|
||||||
const _PlaceSliverAppBar({required this.search});
|
const _PlaceSliverAppBar({required this.search});
|
||||||
|
|
||||||
final ValueNotifier<String?> search;
|
final ValueNotifier<String?> search;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final searchFocusNode = FocusNode();
|
final searchFocusNode = useFocusNode();
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
floating: true,
|
floating: true,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/album_filter.utils.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
|
|
@ -39,8 +40,12 @@ class AlbumSelector extends ConsumerStatefulWidget {
|
||||||
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||||
bool isGrid = false;
|
bool isGrid = false;
|
||||||
final searchController = TextEditingController();
|
final searchController = TextEditingController();
|
||||||
QuickFilterMode filterMode = QuickFilterMode.all;
|
|
||||||
final searchFocusNode = FocusNode();
|
final searchFocusNode = FocusNode();
|
||||||
|
List<RemoteAlbum> sortedAlbums = [];
|
||||||
|
List<RemoteAlbum> shownAlbums = [];
|
||||||
|
|
||||||
|
AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all);
|
||||||
|
AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -52,7 +57,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||||
});
|
});
|
||||||
|
|
||||||
searchController.addListener(() {
|
searchController.addListener(() {
|
||||||
onSearch(searchController.text, filterMode);
|
onSearch(searchController.text, filter.mode);
|
||||||
});
|
});
|
||||||
|
|
||||||
searchFocusNode.addListener(() {
|
searchFocusNode.addListener(() {
|
||||||
|
|
@ -62,9 +67,11 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
void onSearch(String searchTerm, QuickFilterMode filterMode) {
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
|
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);
|
||||||
|
|
||||||
|
filterAlbums();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onRefresh() async {
|
Future<void> onRefresh() async {
|
||||||
|
|
@ -77,17 +84,60 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeFilter(QuickFilterMode sortMode) {
|
void changeFilter(QuickFilterMode mode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
filterMode = sortMode;
|
filter = filter.copyWith(mode: mode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
filterAlbums();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeSort(AlbumSort sort) async {
|
||||||
|
setState(() {
|
||||||
|
this.sort = sort;
|
||||||
|
});
|
||||||
|
|
||||||
|
await sortAlbums();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSearch() {
|
void clearSearch() {
|
||||||
setState(() {
|
setState(() {
|
||||||
filterMode = QuickFilterMode.all;
|
filter = filter.copyWith(mode: QuickFilterMode.all, query: null);
|
||||||
searchController.clear();
|
searchController.clear();
|
||||||
ref.read(remoteAlbumProvider.notifier).clearSearch();
|
});
|
||||||
|
|
||||||
|
filterAlbums();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sortAlbums() async {
|
||||||
|
final sorted = await ref
|
||||||
|
.read(remoteAlbumProvider.notifier)
|
||||||
|
.sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
sortedAlbums = sorted;
|
||||||
|
});
|
||||||
|
|
||||||
|
// we need to re-filter the albums after sorting
|
||||||
|
// so shownAlbums gets updated
|
||||||
|
filterAlbums();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> filterAlbums() async {
|
||||||
|
if (filter.query == null) {
|
||||||
|
setState(() {
|
||||||
|
shownAlbums = sortedAlbums;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final filteredAlbums = ref
|
||||||
|
.read(remoteAlbumProvider.notifier)
|
||||||
|
.searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
shownAlbums = filteredAlbums;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,36 +150,41 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
|
||||||
|
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
|
||||||
|
// refilter and sort when albums change
|
||||||
|
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
|
||||||
|
await sortAlbums();
|
||||||
|
});
|
||||||
|
|
||||||
return MultiSliver(
|
return MultiSliver(
|
||||||
children: [
|
children: [
|
||||||
_SearchBar(
|
_SearchBar(
|
||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
onSearch: onSearch,
|
onSearch: onSearch,
|
||||||
filterMode: filterMode,
|
filterMode: filter.mode,
|
||||||
onClearSearch: clearSearch,
|
onClearSearch: clearSearch,
|
||||||
),
|
),
|
||||||
_QuickFilterButtonRow(
|
_QuickFilterButtonRow(
|
||||||
filterMode: filterMode,
|
filterMode: filter.mode,
|
||||||
onChangeFilter: changeFilter,
|
onChangeFilter: changeFilter,
|
||||||
onSearch: onSearch,
|
onSearch: onSearch,
|
||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
),
|
),
|
||||||
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode),
|
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort),
|
||||||
isGrid
|
isGrid
|
||||||
? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||||
: _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SortButton extends ConsumerStatefulWidget {
|
class _SortButton extends ConsumerStatefulWidget {
|
||||||
const _SortButton();
|
const _SortButton(this.onSortChanged);
|
||||||
|
|
||||||
|
final Future<void> Function(AlbumSort) onSortChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||||
|
|
@ -148,15 +203,15 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||||
albumSortIsReverse = !albumSortIsReverse;
|
albumSortIsReverse = !albumSortIsReverse;
|
||||||
isSorting = true;
|
isSorting = true;
|
||||||
});
|
});
|
||||||
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
albumSortOption = sortMode;
|
albumSortOption = sortMode;
|
||||||
isSorting = true;
|
isSorting = true;
|
||||||
});
|
});
|
||||||
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse));
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isSorting = false;
|
isSorting = false;
|
||||||
});
|
});
|
||||||
|
|
@ -394,10 +449,11 @@ class _QuickFilterButton extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QuickSortAndViewMode extends StatelessWidget {
|
class _QuickSortAndViewMode extends StatelessWidget {
|
||||||
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode});
|
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode, required this.onSortChanged});
|
||||||
|
|
||||||
final bool isGrid;
|
final bool isGrid;
|
||||||
final VoidCallback onToggleViewMode;
|
final VoidCallback onToggleViewMode;
|
||||||
|
final Future<void> Function(AlbumSort) onSortChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -407,7 +463,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const _SortButton(),
|
_SortButton(onSortChanged),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
||||||
onPressed: onToggleViewMode,
|
onPressed: onToggleViewMode,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
|
@ -129,6 +130,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
reloadSubscription?.cancel();
|
reloadSubscription?.cancel();
|
||||||
_prevPreCacheStream?.removeListener(_dummyListener);
|
_prevPreCacheStream?.removeListener(_dummyListener);
|
||||||
_nextPreCacheStream?.removeListener(_dummyListener);
|
_nextPreCacheStream?.removeListener(_dummyListener);
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,6 +598,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
// Rebuild the widget when the asset viewer state changes
|
// Rebuild the widget when the asset viewer state changes
|
||||||
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||||
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||||
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||||
ref.watch(isPlayingMotionVideoProvider);
|
ref.watch(isPlayingMotionVideoProvider);
|
||||||
|
|
@ -612,6 +615,15 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for control visibility changes and change system UI mode accordingly
|
||||||
|
ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async {
|
||||||
|
if (showingControls) {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
} else {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||||
duration: Durations.short2,
|
duration: Durations.short2,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: Durations.short4,
|
duration: Durations.short4,
|
||||||
child: isSheetOpen || isReadonlyModeEnabled
|
child: isSheetOpen
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: Theme(
|
: Theme(
|
||||||
data: context.themeData.copyWith(
|
data: context.themeData.copyWith(
|
||||||
|
|
@ -72,14 +72,14 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: context.padding.bottom + (asset.isVideo ? 160 : 90),
|
|
||||||
color: Colors.black.withAlpha(125),
|
color: Colors.black.withAlpha(125),
|
||||||
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (asset.isVideo) const VideoControls(),
|
if (asset.isVideo) const VideoControls(),
|
||||||
if (!isInLockedView) Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
if (!isInLockedView && !isReadonlyModeEnabled)
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.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/presentation/widgets/action_buttons/archive_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/delete_permanent_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
|
|
@ -16,22 +18,74 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/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/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.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/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class RemoteAlbumBottomSheet extends ConsumerWidget {
|
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
|
||||||
final RemoteAlbum album;
|
final RemoteAlbum album;
|
||||||
const RemoteAlbumBottomSheet({super.key, required this.album});
|
const RemoteAlbumBottomSheet({super.key, required this.album});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<RemoteAlbumBottomSheet> createState() => _RemoteAlbumBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet> {
|
||||||
|
late DraggableScrollableController sheetController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
sheetController = DraggableScrollableController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
sheetController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final multiselect = ref.watch(multiSelectProvider);
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
|
||||||
|
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||||
|
final selectedAssets = multiselect.selectedAssets;
|
||||||
|
if (selectedAssets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final addedCount = await ref
|
||||||
|
.read(remoteAlbumProvider.notifier)
|
||||||
|
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
|
||||||
|
|
||||||
|
if (addedCount != selectedAssets.length) {
|
||||||
|
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}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onKeyboardExpand() {
|
||||||
|
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
initialChildSize: 0.25,
|
controller: sheetController,
|
||||||
maxChildSize: 0.4,
|
initialChildSize: 0.45,
|
||||||
|
maxChildSize: 0.85,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(source: ActionSource.timeline),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
|
|
@ -52,7 +106,11 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id),
|
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||||
|
],
|
||||||
|
slivers: [
|
||||||
|
const AddToAlbumHeader(),
|
||||||
|
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,12 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||||
MapLibreMapController? mapController;
|
MapLibreMapController? mapController;
|
||||||
final _reloadMutex = AsyncMutex();
|
final _reloadMutex = AsyncMutex();
|
||||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||||
|
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_debouncer.dispose();
|
_debouncer.dispose();
|
||||||
|
bottomSheetOffset.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,8 +159,8 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
||||||
_MyLocationButton(onZoomToLocation: onZoomToLocation),
|
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
||||||
const MapBottomSheet(),
|
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -191,21 +193,53 @@ class _Map extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MyLocationButton extends StatelessWidget {
|
class _DynamicBottomSheet extends StatefulWidget {
|
||||||
const _MyLocationButton({required this.onZoomToLocation});
|
final ValueNotifier<double> bottomSheetOffset;
|
||||||
|
|
||||||
final VoidCallback onZoomToLocation;
|
const _DynamicBottomSheet({required this.bottomSheetOffset});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DynamicBottomSheet> createState() => _DynamicBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DynamicBottomSheetState extends State<_DynamicBottomSheet> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Positioned(
|
return NotificationListener<DraggableScrollableNotification>(
|
||||||
right: 0,
|
onNotification: (notification) {
|
||||||
bottom: context.padding.bottom + 16,
|
widget.bottomSheetOffset.value = notification.extent;
|
||||||
child: ElevatedButton(
|
return true;
|
||||||
onPressed: onZoomToLocation,
|
},
|
||||||
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
|
child: const MapBottomSheet(),
|
||||||
child: const Icon(Icons.my_location),
|
);
|
||||||
),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DynamicMyLocationButton extends StatelessWidget {
|
||||||
|
const _DynamicMyLocationButton({required this.onZoomToLocation, required this.bottomSheetOffset});
|
||||||
|
|
||||||
|
final VoidCallback onZoomToLocation;
|
||||||
|
final ValueNotifier<double> bottomSheetOffset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<double>(
|
||||||
|
valueListenable: bottomSheetOffset,
|
||||||
|
builder: (context, offset, child) {
|
||||||
|
return Positioned(
|
||||||
|
right: 16,
|
||||||
|
bottom: context.height * (offset - 0.02) + context.padding.bottom,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: offset < 0.8 ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onZoomToLocation,
|
||||||
|
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
|
||||||
|
child: const Icon(Icons.my_location),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.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.state.dart';
|
||||||
import 'package:intl/intl.dart' hide TextDirection;
|
import 'package:intl/intl.dart' hide TextDirection;
|
||||||
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
|
||||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||||
/// for quick navigation of the BoxScrollView.
|
/// for quick navigation of the BoxScrollView.
|
||||||
|
|
@ -74,6 +75,7 @@ List<_Segment> _buildSegments({required List<Segment> layoutSegments, required d
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin {
|
class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin {
|
||||||
|
String? _lastLabel;
|
||||||
double _thumbTopOffset = 0.0;
|
double _thumbTopOffset = 0.0;
|
||||||
bool _isDragging = false;
|
bool _isDragging = false;
|
||||||
List<_Segment> _segments = [];
|
List<_Segment> _segments = [];
|
||||||
|
|
@ -172,6 +174,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||||
_isDragging = true;
|
_isDragging = true;
|
||||||
_labelAnimationController.forward();
|
_labelAnimationController.forward();
|
||||||
_fadeOutTimer?.cancel();
|
_fadeOutTimer?.cancel();
|
||||||
|
_lastLabel = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,6 +192,11 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||||
|
|
||||||
if (nearestMonthSegment != null) {
|
if (nearestMonthSegment != null) {
|
||||||
_snapToSegment(nearestMonthSegment);
|
_snapToSegment(nearestMonthSegment);
|
||||||
|
final label = nearestMonthSegment.scrollLabel;
|
||||||
|
if (_lastLabel != label) {
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
_lastLabel = label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
|
@ -81,6 +82,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ref.read(backupProvider.notifier).cancelBackup();
|
_ref.read(backupProvider.notifier).cancelBackup();
|
||||||
|
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
||||||
|
|
||||||
|
lockManager.requestHolderToClose();
|
||||||
|
debugPrint("Requested lock holder to close on resume");
|
||||||
|
await lockManager.acquireLock();
|
||||||
|
debugPrint("Lock acquired for background sync on resume");
|
||||||
|
|
||||||
final backgroundManager = _ref.read(backgroundSyncProvider);
|
final backgroundManager = _ref.read(backgroundSyncProvider);
|
||||||
// Ensure proper cleanup before starting new background tasks
|
// Ensure proper cleanup before starting new background tasks
|
||||||
|
|
@ -98,6 +105,8 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
]).then((_) async {
|
]).then((_) async {
|
||||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
|
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
|
||||||
if (isEnableBackup) {
|
if (isEnableBackup) {
|
||||||
final currentUser = _ref.read(currentUserProvider);
|
final currentUser = _ref.read(currentUserProvider);
|
||||||
if (currentUser == null) {
|
if (currentUser == null) {
|
||||||
|
|
@ -106,6 +115,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
|
|
||||||
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAlbumLinkedSyncEnable) {
|
||||||
|
await backgroundManager.syncLinkedAlbum();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
Logger("AppLifeCycleNotifier").severe("Error during background sync", e, stackTrace);
|
Logger("AppLifeCycleNotifier").severe("Error during background sync", e, stackTrace);
|
||||||
|
|
@ -130,7 +143,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
// do not stop/clean up anything on inactivity: issued on every orientation change
|
// do not stop/clean up anything on inactivity: issued on every orientation change
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleAppPause() {
|
Future<void> handleAppPause() async {
|
||||||
state = AppLifeCycleEnum.paused;
|
state = AppLifeCycleEnum.paused;
|
||||||
_wasPaused = true;
|
_wasPaused = true;
|
||||||
|
|
||||||
|
|
@ -140,6 +153,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
|
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||||
_ref.read(backupProvider.notifier).cancelBackup();
|
_ref.read(backupProvider.notifier).cancelBackup();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
final backgroundManager = _ref.read(backgroundSyncProvider);
|
||||||
|
await backgroundManager.cancel();
|
||||||
|
await backgroundManager.cancelLocal();
|
||||||
|
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
||||||
|
debugPrint("Lock released on app pause");
|
||||||
}
|
}
|
||||||
|
|
||||||
_ref.read(websocketProvider.notifier).disconnect();
|
_ref.read(websocketProvider.notifier).disconnect();
|
||||||
|
|
@ -173,6 +192,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
|
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
|
|
||||||
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||||
|
|
@ -18,3 +19,7 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||||
ref.onDispose(manager.cancel);
|
ref.onDispose(manager.cancel);
|
||||||
return manager;
|
return manager;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final isolateLockManagerProvider = Provider.family<IsolateLockManager, String>((ref, name) {
|
||||||
|
return IsolateLockManager(portName: name);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,43 +12,42 @@ import 'album.provider.dart';
|
||||||
|
|
||||||
class RemoteAlbumState {
|
class RemoteAlbumState {
|
||||||
final List<RemoteAlbum> albums;
|
final List<RemoteAlbum> albums;
|
||||||
final List<RemoteAlbum> filteredAlbums;
|
|
||||||
|
|
||||||
const RemoteAlbumState({required this.albums, List<RemoteAlbum>? filteredAlbums})
|
const RemoteAlbumState({required this.albums});
|
||||||
: filteredAlbums = filteredAlbums ?? albums;
|
|
||||||
|
|
||||||
RemoteAlbumState copyWith({List<RemoteAlbum>? albums, List<RemoteAlbum>? filteredAlbums}) {
|
RemoteAlbumState copyWith({List<RemoteAlbum>? albums}) {
|
||||||
return RemoteAlbumState(albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums);
|
return RemoteAlbumState(albums: albums ?? this.albums);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})';
|
String toString() => 'RemoteAlbumState(albums: ${albums.length})';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant RemoteAlbumState other) {
|
bool operator ==(covariant RemoteAlbumState other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
return listEquals(other.albums, albums) && listEquals(other.filteredAlbums, filteredAlbums);
|
return listEquals(other.albums, albums);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => albums.hashCode ^ filteredAlbums.hashCode;
|
int get hashCode => albums.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
late RemoteAlbumService _remoteAlbumService;
|
late RemoteAlbumService _remoteAlbumService;
|
||||||
final _logger = Logger('RemoteAlbumNotifier');
|
final _logger = Logger('RemoteAlbumNotifier');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RemoteAlbumState build() {
|
RemoteAlbumState build() {
|
||||||
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
|
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
|
||||||
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
return const RemoteAlbumState(albums: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _getAll() async {
|
Future<List<RemoteAlbum>> _getAll() async {
|
||||||
try {
|
try {
|
||||||
final albums = await _remoteAlbumService.getAll();
|
final albums = await _remoteAlbumService.getAll();
|
||||||
state = state.copyWith(albums: albums, filteredAlbums: albums);
|
state = state.copyWith(albums: albums);
|
||||||
return albums;
|
return albums;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Failed to fetch albums', error, stack);
|
_logger.severe('Failed to fetch albums', error, stack);
|
||||||
|
|
@ -60,19 +59,21 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
await _getAll();
|
await _getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) {
|
List<RemoteAlbum> searchAlbums(
|
||||||
final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode);
|
List<RemoteAlbum> albums,
|
||||||
|
String query,
|
||||||
state = state.copyWith(filteredAlbums: filtered);
|
String? userId, [
|
||||||
|
QuickFilterMode filterMode = QuickFilterMode.all,
|
||||||
|
]) {
|
||||||
|
return _remoteAlbumService.searchAlbums(albums, query, userId, filterMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSearch() {
|
Future<List<RemoteAlbum>> sortAlbums(
|
||||||
state = state.copyWith(filteredAlbums: state.albums);
|
List<RemoteAlbum> albums,
|
||||||
}
|
RemoteAlbumSortMode sortMode, {
|
||||||
|
bool isReverse = false,
|
||||||
Future<void> sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async {
|
}) async {
|
||||||
final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse);
|
||||||
state = state.copyWith(filteredAlbums: sortedAlbums);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<RemoteAlbum?> createAlbum({
|
Future<RemoteAlbum?> createAlbum({
|
||||||
|
|
@ -83,7 +84,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
try {
|
try {
|
||||||
final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds);
|
final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds);
|
||||||
|
|
||||||
state = state.copyWith(albums: [...state.albums, album], filteredAlbums: [...state.filteredAlbums, album]);
|
state = state.copyWith(albums: [...state.albums, album]);
|
||||||
|
|
||||||
return album;
|
return album;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
|
|
@ -114,11 +115,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
return album.id == albumId ? updatedAlbum : album;
|
return album.id == albumId ? updatedAlbum : album;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final updatedFilteredAlbums = state.filteredAlbums.map((album) {
|
state = state.copyWith(albums: updatedAlbums);
|
||||||
return album.id == albumId ? updatedAlbum : album;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
|
||||||
|
|
||||||
return updatedAlbum;
|
return updatedAlbum;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
|
|
@ -139,9 +136,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
await _remoteAlbumService.deleteAlbum(albumId);
|
await _remoteAlbumService.deleteAlbum(albumId);
|
||||||
|
|
||||||
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||||
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
state = state.copyWith(albums: updatedAlbums);
|
||||||
|
|
||||||
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||||
|
|
@ -164,9 +159,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
await _remoteAlbumService.removeUser(albumId, userId: userId);
|
await _remoteAlbumService.removeUser(albumId, userId: userId);
|
||||||
|
|
||||||
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||||
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
state = state.copyWith(albums: updatedAlbums);
|
||||||
|
|
||||||
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setActivityStatus(String albumId, bool enabled) {
|
Future<void> setActivityStatus(String albumId, bool enabled) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
// import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
|
@ -323,7 +322,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()));
|
unawaited(
|
||||||
|
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
|
||||||
|
return _ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_log.severe("Error processing batched AssetUploadReadyV1 events: $error");
|
_log.severe("Error processing batched AssetUploadReadyV1 events: $error");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,8 @@ class UploadService {
|
||||||
|
|
||||||
return buildUploadTask(
|
return buildUploadTask(
|
||||||
file,
|
file,
|
||||||
|
createdAt: asset.createdAt,
|
||||||
|
modifiedAt: asset.updatedAt,
|
||||||
originalFileName: originalFileName,
|
originalFileName: originalFileName,
|
||||||
deviceAssetId: asset.id,
|
deviceAssetId: asset.id,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
|
|
@ -309,6 +311,8 @@ class UploadService {
|
||||||
|
|
||||||
return buildUploadTask(
|
return buildUploadTask(
|
||||||
file,
|
file,
|
||||||
|
createdAt: asset.createdAt,
|
||||||
|
modifiedAt: asset.updatedAt,
|
||||||
originalFileName: asset.name,
|
originalFileName: asset.name,
|
||||||
deviceAssetId: asset.id,
|
deviceAssetId: asset.id,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
|
|
@ -334,6 +338,8 @@ class UploadService {
|
||||||
Future<UploadTask> buildUploadTask(
|
Future<UploadTask> buildUploadTask(
|
||||||
File file, {
|
File file, {
|
||||||
required String group,
|
required String group,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime modifiedAt,
|
||||||
Map<String, String>? fields,
|
Map<String, String>? fields,
|
||||||
String? originalFileName,
|
String? originalFileName,
|
||||||
String? deviceAssetId,
|
String? deviceAssetId,
|
||||||
|
|
@ -347,15 +353,12 @@ class UploadService {
|
||||||
final headers = ApiService.getRequestHeaders();
|
final headers = ApiService.getRequestHeaders();
|
||||||
final deviceId = Store.get(StoreKey.deviceId);
|
final deviceId = Store.get(StoreKey.deviceId);
|
||||||
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
|
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
|
||||||
final stats = await file.stat();
|
|
||||||
final fileCreatedAt = stats.changed;
|
|
||||||
final fileModifiedAt = stats.modified;
|
|
||||||
final fieldsMap = {
|
final fieldsMap = {
|
||||||
'filename': originalFileName ?? filename,
|
'filename': originalFileName ?? filename,
|
||||||
'deviceAssetId': deviceAssetId ?? '',
|
'deviceAssetId': deviceAssetId ?? '',
|
||||||
'deviceId': deviceId,
|
'deviceId': deviceId,
|
||||||
'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(),
|
'fileCreatedAt': createdAt.toUtc().toIso8601String(),
|
||||||
'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(),
|
'fileModifiedAt': modifiedAt.toUtc().toIso8601String(),
|
||||||
'isFavorite': isFavorite?.toString() ?? 'false',
|
'isFavorite': isFavorite?.toString() ?? 'false',
|
||||||
'duration': '0',
|
'duration': '0',
|
||||||
if (fields != null) ...fields,
|
if (fields != null) ...fields,
|
||||||
|
|
|
||||||
25
mobile/lib/utils/album_filter.utils.dart
Normal file
25
mobile/lib/utils/album_filter.utils.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
|
|
||||||
|
class AlbumFilter {
|
||||||
|
String? userId;
|
||||||
|
String? query;
|
||||||
|
QuickFilterMode mode;
|
||||||
|
|
||||||
|
AlbumFilter({required this.mode, this.userId, this.query});
|
||||||
|
|
||||||
|
AlbumFilter copyWith({String? userId, String? query, QuickFilterMode? mode}) {
|
||||||
|
return AlbumFilter(userId: userId ?? this.userId, query: query ?? this.query, mode: mode ?? this.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumSort {
|
||||||
|
RemoteAlbumSortMode mode;
|
||||||
|
bool isReverse;
|
||||||
|
|
||||||
|
AlbumSort({required this.mode, this.isReverse = false});
|
||||||
|
|
||||||
|
AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) {
|
||||||
|
return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
|
||||||
|
|
||||||
extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
|
|
||||||
LocalAlbum toDto({int assetCount = 0}) {
|
|
||||||
return LocalAlbum(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
assetCount: assetCount,
|
|
||||||
backupSelection: backupSelection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension LocalAssetEntityDataHelper on LocalAssetEntityData {
|
|
||||||
LocalAsset toDto() {
|
|
||||||
return LocalAsset(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
checksum: checksum,
|
|
||||||
type: type,
|
|
||||||
createdAt: createdAt,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
durationInSeconds: durationInSeconds,
|
|
||||||
isFavorite: isFavorite,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,8 +23,10 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
@ -268,11 +270,17 @@ Future<List<void>> runNewSync(WidgetRef ref, {bool full = false}) {
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
|
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||||
|
final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
|
||||||
return Future.wait([
|
return Future.wait([
|
||||||
backgroundManager.syncLocal(full: full).then((_) {
|
backgroundManager.syncLocal(full: full).then((_) {
|
||||||
Logger("runNewSync").fine("Hashing assets after syncLocal");
|
Logger("runNewSync").fine("Hashing assets after syncLocal");
|
||||||
return backgroundManager.hashAssets();
|
return backgroundManager.hashAssets();
|
||||||
}),
|
}),
|
||||||
backgroundManager.syncRemote(),
|
backgroundManager.syncRemote().then((_) {
|
||||||
|
if (isAlbumLinkedSyncEnable) {
|
||||||
|
return backgroundManager.syncLinkedAlbum();
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/drag_sheet.dart';
|
import 'package:immich_mobile/widgets/common/drag_sheet.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
|
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
|
||||||
|
|
||||||
final controlBottomAppBarNotifier = ControlBottomAppBarNotifier();
|
final controlBottomAppBarNotifier = ControlBottomAppBarNotifier();
|
||||||
|
|
@ -45,6 +47,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
||||||
final bool unfavorite;
|
final bool unfavorite;
|
||||||
final bool unarchive;
|
final bool unarchive;
|
||||||
final AssetSelectionState selectionAssetState;
|
final AssetSelectionState selectionAssetState;
|
||||||
|
final List<Asset> selectedAssets;
|
||||||
|
|
||||||
const ControlBottomAppBar({
|
const ControlBottomAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -64,6 +67,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
||||||
this.onRemoveFromAlbum,
|
this.onRemoveFromAlbum,
|
||||||
this.onToggleLocked,
|
this.onToggleLocked,
|
||||||
this.selectionAssetState = const AssetSelectionState(),
|
this.selectionAssetState = const AssetSelectionState(),
|
||||||
|
this.selectedAssets = const [],
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.unarchive = false,
|
this.unarchive = false,
|
||||||
this.unfavorite = false,
|
this.unfavorite = false,
|
||||||
|
|
@ -100,6 +104,18 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show existing AddToAlbumBottomSheet
|
||||||
|
void showAddToAlbumBottomSheet() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
elevation: 0,
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))),
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext _) {
|
||||||
|
return AddToAlbumBottomSheet(assets: selectedAssets);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) {
|
void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
deleteCb(force);
|
deleteCb(force);
|
||||||
|
|
@ -121,6 +137,15 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
||||||
label: "share_link".tr(),
|
label: "share_link".tr(),
|
||||||
onPressed: enabled ? () => onShare(false) : null,
|
onPressed: enabled ? () => onShare(false) : null,
|
||||||
),
|
),
|
||||||
|
if (!isInLockedView && hasRemote && albums.isNotEmpty)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 100),
|
||||||
|
child: ControlBoxButton(
|
||||||
|
iconData: Icons.photo_album,
|
||||||
|
label: "add_to_album".tr(),
|
||||||
|
onPressed: enabled ? showAddToAlbumBottomSheet : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (hasRemote && onArchive != null)
|
if (hasRemote && onArchive != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined,
|
iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined,
|
||||||
|
|
|
||||||
|
|
@ -440,6 +440,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||||
onUpload: onUpload,
|
onUpload: onUpload,
|
||||||
enabled: !processing.value,
|
enabled: !processing.value,
|
||||||
selectionAssetState: selectionAssetState.value,
|
selectionAssetState: selectionAssetState.value,
|
||||||
|
selectedAssets: selection.value.toList(),
|
||||||
onStack: stackEnabled ? onStack : null,
|
onStack: stackEnabled ? onStack : null,
|
||||||
onEditTime: editEnabled ? onEditTime : null,
|
onEditTime: editEnabled ? onEditTime : null,
|
||||||
onEditLocation: editEnabled ? onEditLocation : null,
|
onEditLocation: editEnabled ? onEditLocation : null,
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,9 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class DriftAlbumInfoListTile extends HookConsumerWidget {
|
class DriftAlbumInfoListTile extends HookConsumerWidget {
|
||||||
|
|
@ -22,8 +19,6 @@ class DriftAlbumInfoListTile extends HookConsumerWidget {
|
||||||
final bool isSelected = album.backupSelection == BackupSelection.selected;
|
final bool isSelected = album.backupSelection == BackupSelection.selected;
|
||||||
final bool isExcluded = album.backupSelection == BackupSelection.excluded;
|
final bool isExcluded = album.backupSelection == BackupSelection.excluded;
|
||||||
|
|
||||||
final syncAlbum = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
|
||||||
|
|
||||||
buildTileColor() {
|
buildTileColor() {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25);
|
return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25);
|
||||||
|
|
@ -75,9 +70,6 @@ class DriftAlbumInfoListTile extends HookConsumerWidget {
|
||||||
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
||||||
} else {
|
} else {
|
||||||
ref.read(backupAlbumProvider.notifier).selectAlbum(album);
|
ref.read(backupAlbumProvider.notifier).selectAlbum(album);
|
||||||
if (syncAlbum) {
|
|
||||||
ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leading: buildIcon(),
|
leading: buildIcon(),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
|
|
@ -259,7 +260,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||||
const AppBarProfileInfoBox(),
|
const AppBarProfileInfoBox(),
|
||||||
buildStorageInformation(),
|
buildStorageInformation(),
|
||||||
const AppBarServerInfo(),
|
const AppBarServerInfo(),
|
||||||
if (isReadonlyModeEnabled) buildReadonlyMessage(),
|
if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(),
|
||||||
buildAppLogButton(),
|
buildAppLogButton(),
|
||||||
buildSettingButton(),
|
buildSettingButton(),
|
||||||
buildSignOutButton(),
|
buildSignOutButton(),
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,6 @@ class PhotoViewCore extends StatefulWidget {
|
||||||
|
|
||||||
class PhotoViewCoreState extends State<PhotoViewCore>
|
class PhotoViewCoreState extends State<PhotoViewCore>
|
||||||
with TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector {
|
with TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector {
|
||||||
Offset? _normalizedPosition;
|
|
||||||
double? _scaleBefore;
|
double? _scaleBefore;
|
||||||
double? _rotationBefore;
|
double? _rotationBefore;
|
||||||
|
|
||||||
|
|
@ -154,7 +153,6 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||||
void onScaleStart(ScaleStartDetails details) {
|
void onScaleStart(ScaleStartDetails details) {
|
||||||
_rotationBefore = controller.rotation;
|
_rotationBefore = controller.rotation;
|
||||||
_scaleBefore = scale;
|
_scaleBefore = scale;
|
||||||
_normalizedPosition = details.focalPoint - controller.position;
|
|
||||||
_scaleAnimationController.stop();
|
_scaleAnimationController.stop();
|
||||||
_positionAnimationController.stop();
|
_positionAnimationController.stop();
|
||||||
_rotationAnimationController.stop();
|
_rotationAnimationController.stop();
|
||||||
|
|
@ -166,8 +164,14 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||||
};
|
};
|
||||||
|
|
||||||
void onScaleUpdate(ScaleUpdateDetails details) {
|
void onScaleUpdate(ScaleUpdateDetails details) {
|
||||||
|
final centeredFocalPoint = Offset(
|
||||||
|
details.focalPoint.dx - scaleBoundaries.outerSize.width / 2,
|
||||||
|
details.focalPoint.dy - scaleBoundaries.outerSize.height / 2,
|
||||||
|
);
|
||||||
final double newScale = _scaleBefore! * details.scale;
|
final double newScale = _scaleBefore! * details.scale;
|
||||||
Offset delta = details.focalPoint - _normalizedPosition!;
|
final double scaleDelta = newScale / scale;
|
||||||
|
final Offset newPosition =
|
||||||
|
(controller.position + details.focalPointDelta) * scaleDelta - centeredFocalPoint * (scaleDelta - 1);
|
||||||
|
|
||||||
updateScaleStateFromNewScale(newScale);
|
updateScaleStateFromNewScale(newScale);
|
||||||
|
|
||||||
|
|
@ -176,7 +180,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||||
|
|
||||||
updateMultiple(
|
updateMultiple(
|
||||||
scale: newScale,
|
scale: newScale,
|
||||||
position: panEnabled ? delta : clampPosition(position: delta * details.scale),
|
position: panEnabled ? newPosition : clampPosition(position: newPosition),
|
||||||
rotation: rotationEnabled ? _rotationBefore! + details.rotation : null,
|
rotation: rotationEnabled ? _rotationBefore! + details.rotation : null,
|
||||||
rotationFocusPoint: rotationEnabled ? details.focalPoint : null,
|
rotationFocusPoint: rotationEnabled ? details.focalPoint : null,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,153 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.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/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
|
|
||||||
class DriftBackupSettings extends StatelessWidget {
|
class DriftBackupSettings extends ConsumerWidget {
|
||||||
const DriftBackupSettings({super.key});
|
const DriftBackupSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return const SettingsSubPageScaffold(
|
||||||
|
settings: [
|
||||||
|
_UseWifiForUploadVideosButton(),
|
||||||
|
_UseWifiForUploadPhotosButton(),
|
||||||
|
Divider(indent: 16, endIndent: 16),
|
||||||
|
_AlbumSyncActionButton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumSyncActionButton extends ConsumerStatefulWidget {
|
||||||
|
const _AlbumSyncActionButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_AlbumSyncActionButton> createState() => _AlbumSyncActionButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> {
|
||||||
|
bool isAlbumSyncInProgress = false;
|
||||||
|
|
||||||
|
Future<void> _manualSyncAlbums() async {
|
||||||
|
setState(() {
|
||||||
|
isAlbumSyncInProgress = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||||
|
await ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
} catch (_) {
|
||||||
|
} finally {
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
setState(() {
|
||||||
|
isAlbumSyncInProgress = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _manageLinkedAlbums() async {
|
||||||
|
final currentUser = ref.read(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final localAlbums = ref.read(backupAlbumProvider);
|
||||||
|
final selectedBackupAlbums = localAlbums
|
||||||
|
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await ref.read(syncLinkedAlbumServiceProvider).manageLinkedAlbums(selectedBackupAlbums, currentUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const SettingsSubPageScaffold(settings: [_UseWifiForUploadVideosButton(), _UseWifiForUploadPhotosButton()]);
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
StreamBuilder(
|
||||||
|
stream: Store.watch(StoreKey.syncAlbums),
|
||||||
|
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final albumSyncEnable = snapshot.data ?? false;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"sync_albums".t(context: context),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"sync_upload_album_setting_subtitle".t(context: context),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: albumSyncEnable,
|
||||||
|
onChanged: (bool newValue) async {
|
||||||
|
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
|
||||||
|
|
||||||
|
if (newValue == true) {
|
||||||
|
await _manageLinkedAlbums();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
opacity: albumSyncEnable ? 1.0 : 0.0,
|
||||||
|
child: albumSyncEnable
|
||||||
|
? ListTile(
|
||||||
|
onTap: _manualSyncAlbums,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||||
|
title: Text(
|
||||||
|
"organize_into_albums".t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"organize_into_albums_description".t(context: context),
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: isAlbumSyncInProgress
|
||||||
|
? const SizedBox(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
onPressed: _manualSyncAlbums,
|
||||||
|
icon: const Icon(Icons.sync_rounded),
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
iconSize: 20,
|
||||||
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,37 @@ class BetaSyncSettings extends HookConsumerWidget {
|
||||||
await ref.read(storageRepositoryProvider).clearCache();
|
await ref.read(storageRepositoryProvider).clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> resetSqliteDb(BuildContext context, Future<void> Function() resetDatabase) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("reset_sqlite".t(context: context)),
|
||||||
|
content: Text("reset_sqlite_confirmation".t(context: context)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: Text("cancel".t(context: context)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await resetDatabase();
|
||||||
|
context.pop();
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"confirm".t(context: context),
|
||||||
|
style: TextStyle(color: context.colorScheme.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return FutureBuilder<List<dynamic>>(
|
return FutureBuilder<List<dynamic>>(
|
||||||
future: loadCounts(),
|
future: loadCounts(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|
@ -116,6 +147,33 @@ class BetaSyncSettings extends HookConsumerWidget {
|
||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"Error occur, reset the local database by tapping the button below",
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"reset_sqlite".t(context: context),
|
||||||
|
style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
|
||||||
|
onTap: () async {
|
||||||
|
await resetSqliteDb(context, resetDatabase);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final assetCounts = snapshot.data![0]! as (int, int);
|
final assetCounts = snapshot.data![0]! as (int, int);
|
||||||
final localAssetCount = assetCounts.$1;
|
final localAssetCount = assetCounts.$1;
|
||||||
final remoteAssetCount = assetCounts.$2;
|
final remoteAssetCount = assetCounts.$2;
|
||||||
|
|
@ -270,34 +328,7 @@ class BetaSyncSettings extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
|
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
showDialog(
|
await resetSqliteDb(context, resetDatabase);
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text("reset_sqlite".t(context: context)),
|
|
||||||
content: Text("reset_sqlite_confirmation".t(context: context)),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => context.pop(),
|
|
||||||
child: Text("cancel".t(context: context)),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await resetDatabase();
|
|
||||||
context.pop();
|
|
||||||
context.scaffoldMessenger.showSnackBar(
|
|
||||||
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"confirm".t(context: context),
|
|
||||||
style: TextStyle(color: context.colorScheme.error),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
|
|
@ -3,7 +3,7 @@ Immich API
|
||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.140.1
|
- API version: 1.141.0
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|
|
||||||
38
mobile/openapi/lib/model/smart_search_dto.dart
generated
38
mobile/openapi/lib/model/smart_search_dto.dart
generated
|
|
@ -31,7 +31,8 @@ class SmartSearchDto {
|
||||||
this.model,
|
this.model,
|
||||||
this.page,
|
this.page,
|
||||||
this.personIds = const [],
|
this.personIds = const [],
|
||||||
required this.query,
|
this.query,
|
||||||
|
this.queryAssetId,
|
||||||
this.rating,
|
this.rating,
|
||||||
this.size,
|
this.size,
|
||||||
this.state,
|
this.state,
|
||||||
|
|
@ -151,7 +152,21 @@ class SmartSearchDto {
|
||||||
|
|
||||||
List<String> personIds;
|
List<String> personIds;
|
||||||
|
|
||||||
String query;
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? query;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? queryAssetId;
|
||||||
|
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
|
|
@ -278,6 +293,7 @@ class SmartSearchDto {
|
||||||
other.page == page &&
|
other.page == page &&
|
||||||
_deepEquality.equals(other.personIds, personIds) &&
|
_deepEquality.equals(other.personIds, personIds) &&
|
||||||
other.query == query &&
|
other.query == query &&
|
||||||
|
other.queryAssetId == queryAssetId &&
|
||||||
other.rating == rating &&
|
other.rating == rating &&
|
||||||
other.size == size &&
|
other.size == size &&
|
||||||
other.state == state &&
|
other.state == state &&
|
||||||
|
|
@ -314,7 +330,8 @@ class SmartSearchDto {
|
||||||
(model == null ? 0 : model!.hashCode) +
|
(model == null ? 0 : model!.hashCode) +
|
||||||
(page == null ? 0 : page!.hashCode) +
|
(page == null ? 0 : page!.hashCode) +
|
||||||
(personIds.hashCode) +
|
(personIds.hashCode) +
|
||||||
(query.hashCode) +
|
(query == null ? 0 : query!.hashCode) +
|
||||||
|
(queryAssetId == null ? 0 : queryAssetId!.hashCode) +
|
||||||
(rating == null ? 0 : rating!.hashCode) +
|
(rating == null ? 0 : rating!.hashCode) +
|
||||||
(size == null ? 0 : size!.hashCode) +
|
(size == null ? 0 : size!.hashCode) +
|
||||||
(state == null ? 0 : state!.hashCode) +
|
(state == null ? 0 : state!.hashCode) +
|
||||||
|
|
@ -331,7 +348,7 @@ class SmartSearchDto {
|
||||||
(withExif == null ? 0 : withExif!.hashCode);
|
(withExif == null ? 0 : withExif!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
|
String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
|
@ -417,7 +434,16 @@ class SmartSearchDto {
|
||||||
// json[r'page'] = null;
|
// json[r'page'] = null;
|
||||||
}
|
}
|
||||||
json[r'personIds'] = this.personIds;
|
json[r'personIds'] = this.personIds;
|
||||||
|
if (this.query != null) {
|
||||||
json[r'query'] = this.query;
|
json[r'query'] = this.query;
|
||||||
|
} else {
|
||||||
|
// json[r'query'] = null;
|
||||||
|
}
|
||||||
|
if (this.queryAssetId != null) {
|
||||||
|
json[r'queryAssetId'] = this.queryAssetId;
|
||||||
|
} else {
|
||||||
|
// json[r'queryAssetId'] = null;
|
||||||
|
}
|
||||||
if (this.rating != null) {
|
if (this.rating != null) {
|
||||||
json[r'rating'] = this.rating;
|
json[r'rating'] = this.rating;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -522,7 +548,8 @@ class SmartSearchDto {
|
||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
query: mapValueOfType<String>(json, r'query')!,
|
query: mapValueOfType<String>(json, r'query'),
|
||||||
|
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: num.parse('${json[r'rating']}'),
|
||||||
size: num.parse('${json[r'size']}'),
|
size: num.parse('${json[r'size']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
|
|
@ -586,7 +613,6 @@ class SmartSearchDto {
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'query',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
|
|
@ -69,6 +69,7 @@ class SyncEntityType {
|
||||||
static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1');
|
static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1');
|
||||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||||
static const syncResetV1 = SyncEntityType._(r'SyncResetV1');
|
static const syncResetV1 = SyncEntityType._(r'SyncResetV1');
|
||||||
|
static const syncCompleteV1 = SyncEntityType._(r'SyncCompleteV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncEntityType].
|
/// List of all possible values in this [enum][SyncEntityType].
|
||||||
static const values = <SyncEntityType>[
|
static const values = <SyncEntityType>[
|
||||||
|
|
@ -118,6 +119,7 @@ class SyncEntityType {
|
||||||
userMetadataDeleteV1,
|
userMetadataDeleteV1,
|
||||||
syncAckV1,
|
syncAckV1,
|
||||||
syncResetV1,
|
syncResetV1,
|
||||||
|
syncCompleteV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
||||||
|
|
@ -202,6 +204,7 @@ class SyncEntityTypeTypeTransformer {
|
||||||
case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1;
|
case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1;
|
||||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||||
case r'SyncResetV1': return SyncEntityType.syncResetV1;
|
case r'SyncResetV1': return SyncEntityType.syncResetV1;
|
||||||
|
case r'SyncCompleteV1': return SyncEntityType.syncCompleteV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,9 @@ import 'package:pigeon/pigeon.dart';
|
||||||
)
|
)
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class BackgroundWorkerFgHostApi {
|
abstract class BackgroundWorkerFgHostApi {
|
||||||
void enableSyncWorker();
|
void enable();
|
||||||
|
|
||||||
void enableUploadWorker();
|
void disable();
|
||||||
|
|
||||||
// Disables the background upload service
|
|
||||||
void disableUploadWorker();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
|
|
@ -27,15 +24,12 @@ abstract class BackgroundWorkerBgHostApi {
|
||||||
// required platform channels to notify the native side to start the background upload
|
// required platform channels to notify the native side to start the background upload
|
||||||
void onInitialized();
|
void onInitialized();
|
||||||
|
|
||||||
|
// Called from the background flutter engine to request the native side to cleanup
|
||||||
void close();
|
void close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FlutterApi()
|
@FlutterApi()
|
||||||
abstract class BackgroundWorkerFlutterApi {
|
abstract class BackgroundWorkerFlutterApi {
|
||||||
// Android & iOS: Called when the local sync is triggered
|
|
||||||
@async
|
|
||||||
void onLocalSync(int? maxSeconds);
|
|
||||||
|
|
||||||
// iOS Only: Called when the iOS background upload is triggered
|
// iOS Only: Called when the iOS background upload is triggered
|
||||||
@async
|
@async
|
||||||
void onIosUpload(bool isRefresh, int? maxSeconds);
|
void onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ name: immich_mobile
|
||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.140.1+3011
|
version: 1.141.0+3012
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
|
|
|
||||||
5
mobile/test/drift/main/generated/schema.dart
generated
5
mobile/test/drift/main/generated/schema.dart
generated
|
|
@ -11,6 +11,7 @@ import 'schema_v5.dart' as v5;
|
||||||
import 'schema_v6.dart' as v6;
|
import 'schema_v6.dart' as v6;
|
||||||
import 'schema_v7.dart' as v7;
|
import 'schema_v7.dart' as v7;
|
||||||
import 'schema_v8.dart' as v8;
|
import 'schema_v8.dart' as v8;
|
||||||
|
import 'schema_v9.dart' as v9;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
|
|
@ -32,10 +33,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
return v7.DatabaseAtV7(db);
|
return v7.DatabaseAtV7(db);
|
||||||
case 8:
|
case 8:
|
||||||
return v8.DatabaseAtV8(db);
|
return v8.DatabaseAtV8(db);
|
||||||
|
case 9:
|
||||||
|
return v9.DatabaseAtV9(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8];
|
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6712
mobile/test/drift/main/generated/schema_v9.dart
generated
Normal file
6712
mobile/test/drift/main/generated/schema_v9.dart
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -9790,7 +9790,7 @@
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.140.1",
|
"version": "1.141.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|
@ -14571,6 +14571,10 @@
|
||||||
"query": {
|
"query": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"queryAssetId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
|
|
@ -14638,9 +14642,6 @@
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
|
||||||
"query"
|
|
||||||
],
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"SourceType": {
|
"SourceType": {
|
||||||
|
|
@ -15416,6 +15417,10 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncCompleteV1": {
|
||||||
|
"properties": {},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncEntityType": {
|
"SyncEntityType": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"AuthUserV1",
|
"AuthUserV1",
|
||||||
|
|
@ -15463,7 +15468,8 @@
|
||||||
"UserMetadataV1",
|
"UserMetadataV1",
|
||||||
"UserMetadataDeleteV1",
|
"UserMetadataDeleteV1",
|
||||||
"SyncAckV1",
|
"SyncAckV1",
|
||||||
"SyncResetV1"
|
"SyncResetV1",
|
||||||
|
"SyncCompleteV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
22.18.0
|
22.19.0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.140.1",
|
"version": "1.141.0",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.18.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -28,6 +28,6 @@
|
||||||
"directory": "open-api/typescript-sdk"
|
"directory": "open-api/typescript-sdk"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.18.0"
|
"node": "22.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.140.1
|
* 1.141.0
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
|
|
@ -1014,7 +1014,8 @@ export type SmartSearchDto = {
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
page?: number;
|
page?: number;
|
||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
query: string;
|
query?: string;
|
||||||
|
queryAssetId?: string;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
state?: string | null;
|
state?: string | null;
|
||||||
|
|
@ -4921,7 +4922,8 @@ export enum SyncEntityType {
|
||||||
UserMetadataV1 = "UserMetadataV1",
|
UserMetadataV1 = "UserMetadataV1",
|
||||||
UserMetadataDeleteV1 = "UserMetadataDeleteV1",
|
UserMetadataDeleteV1 = "UserMetadataDeleteV1",
|
||||||
SyncAckV1 = "SyncAckV1",
|
SyncAckV1 = "SyncAckV1",
|
||||||
SyncResetV1 = "SyncResetV1"
|
SyncResetV1 = "SyncResetV1",
|
||||||
|
SyncCompleteV1 = "SyncCompleteV1"
|
||||||
}
|
}
|
||||||
export enum SyncRequestType {
|
export enum SyncRequestType {
|
||||||
AlbumsV1 = "AlbumsV1",
|
AlbumsV1 = "AlbumsV1",
|
||||||
|
|
|
||||||
1665
pnpm-lock.yaml
generated
1665
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -27,7 +27,7 @@ onlyBuiltDependencies:
|
||||||
- '@tailwindcss/oxide'
|
- '@tailwindcss/oxide'
|
||||||
overrides:
|
overrides:
|
||||||
canvas: 2.11.2
|
canvas: 2.11.2
|
||||||
sharp: ^0.34.2
|
sharp: ^0.34.3
|
||||||
packageExtensions:
|
packageExtensions:
|
||||||
nestjs-kysely:
|
nestjs-kysely:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
22.18.0
|
22.19.0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.140.1",
|
"version": "1.141.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sanitize-html": "^2.14.0",
|
"sanitize-html": "^2.14.0",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.2",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.3",
|
||||||
"sirv": "^3.0.0",
|
"sirv": "^3.0.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"tailwindcss-preset-email": "^1.4.0",
|
"tailwindcss-preset-email": "^1.4.0",
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
"@types/luxon": "^3.6.2",
|
"@types/luxon": "^3.6.2",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.18.0",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/picomatch": "^4.0.0",
|
"@types/picomatch": "^4.0.0",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "^6.0.5",
|
||||||
|
|
@ -173,9 +173,9 @@
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.18.0"
|
"node": "22.19.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "^0.34.2"
|
"sharp": "^0.34.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { AssetMediaController } from 'src/controllers/asset-media.controller';
|
import { AssetMediaController } from 'src/controllers/asset-media.controller';
|
||||||
|
import { AssetMediaStatus } from 'src/dtos/asset-media-response.dto';
|
||||||
|
import { AssetMetadataKey } from 'src/enum';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
@ -11,7 +13,7 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
||||||
deviceId: 'TEST',
|
deviceId: 'TEST',
|
||||||
fileCreatedAt: new Date().toISOString(),
|
fileCreatedAt: new Date().toISOString(),
|
||||||
fileModifiedAt: new Date().toISOString(),
|
fileModifiedAt: new Date().toISOString(),
|
||||||
isFavorite: 'testing',
|
isFavorite: 'false',
|
||||||
duration: '0:00:00.000000',
|
duration: '0:00:00.000000',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -27,16 +29,20 @@ describe(AssetMediaController.name, () => {
|
||||||
let ctx: ControllerContext;
|
let ctx: ControllerContext;
|
||||||
const assetData = Buffer.from('123');
|
const assetData = Buffer.from('123');
|
||||||
const filename = 'example.png';
|
const filename = 'example.png';
|
||||||
|
const service = mockBaseService(AssetMediaService);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
ctx = await controllerSetup(AssetMediaController, [
|
ctx = await controllerSetup(AssetMediaController, [
|
||||||
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
|
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
|
||||||
{ provide: AssetMediaService, useValue: mockBaseService(AssetMediaService) },
|
{ provide: AssetMediaService, useValue: service },
|
||||||
]);
|
]);
|
||||||
return () => ctx.close();
|
return () => ctx.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
service.uploadAsset.mockResolvedValue({ status: AssetMediaStatus.DUPLICATE, id: factory.uuid() });
|
||||||
|
|
||||||
ctx.reset();
|
ctx.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -46,13 +52,61 @@ describe(AssetMediaController.name, () => {
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should accept metadata', async () => {
|
||||||
|
const mobileMetadata = { key: AssetMetadataKey.MobileApp, value: { iCloudId: '123' } };
|
||||||
|
const { status } = await request(ctx.getHttpServer())
|
||||||
|
.post('/assets')
|
||||||
|
.attach('assetData', assetData, filename)
|
||||||
|
.field({
|
||||||
|
...makeUploadDto(),
|
||||||
|
metadata: JSON.stringify([mobileMetadata]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.uploadAsset).toHaveBeenCalledWith(
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ metadata: [mobileMetadata] }),
|
||||||
|
expect.objectContaining({ originalName: 'example.png' }),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid metadata json', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.post('/assets')
|
||||||
|
.attach('assetData', assetData, filename)
|
||||||
|
.field({
|
||||||
|
...makeUploadDto(),
|
||||||
|
metadata: 'not-a-string-string',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(factory.responses.badRequest(['metadata must be valid JSON']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate iCloudId is a string', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.post('/assets')
|
||||||
|
.attach('assetData', assetData, filename)
|
||||||
|
.field({
|
||||||
|
...makeUploadDto(),
|
||||||
|
metadata: JSON.stringify([{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 123 } }]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(factory.responses.badRequest(['metadata.0.value.iCloudId must be a string']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should require `deviceAssetId`', async () => {
|
it('should require `deviceAssetId`', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer())
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
.post('/assets')
|
.post('/assets')
|
||||||
.attach('assetData', assetData, filename)
|
.attach('assetData', assetData, filename)
|
||||||
.field({ ...makeUploadDto({ omit: 'deviceAssetId' }) });
|
.field({ ...makeUploadDto({ omit: 'deviceAssetId' }) });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(
|
||||||
|
factory.responses.badRequest(['deviceAssetId must be a string', 'deviceAssetId should not be empty']),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require `deviceId`', async () => {
|
it('should require `deviceId`', async () => {
|
||||||
|
|
@ -61,7 +115,7 @@ describe(AssetMediaController.name, () => {
|
||||||
.attach('assetData', assetData, filename)
|
.attach('assetData', assetData, filename)
|
||||||
.field({ ...makeUploadDto({ omit: 'deviceId' }) });
|
.field({ ...makeUploadDto({ omit: 'deviceId' }) });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(factory.responses.badRequest(['deviceId must be a string', 'deviceId should not be empty']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require `fileCreatedAt`', async () => {
|
it('should require `fileCreatedAt`', async () => {
|
||||||
|
|
@ -70,25 +124,20 @@ describe(AssetMediaController.name, () => {
|
||||||
.attach('assetData', assetData, filename)
|
.attach('assetData', assetData, filename)
|
||||||
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
|
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(
|
||||||
|
factory.responses.badRequest(['fileCreatedAt must be a Date instance', 'fileCreatedAt should not be empty']),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require `fileModifiedAt`', async () => {
|
it('should require `fileModifiedAt`', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer())
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
.post('/assets')
|
.post('/assets')
|
||||||
.attach('assetData', assetData, filename)
|
.attach('assetData', assetData, filename)
|
||||||
.field({ ...makeUploadDto({ omit: 'fileModifiedAt' }) });
|
.field(makeUploadDto({ omit: 'fileModifiedAt' }));
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(
|
||||||
});
|
factory.responses.badRequest(['fileModifiedAt must be a Date instance', 'fileModifiedAt should not be empty']),
|
||||||
|
);
|
||||||
it('should require `duration`', async () => {
|
|
||||||
const { status, body } = await request(ctx.getHttpServer())
|
|
||||||
.post('/assets')
|
|
||||||
.attach('assetData', assetData, filename)
|
|
||||||
.field({ ...makeUploadDto({ omit: 'duration' }) });
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if `isFavorite` is not a boolean', async () => {
|
it('should throw if `isFavorite` is not a boolean', async () => {
|
||||||
|
|
@ -97,16 +146,18 @@ describe(AssetMediaController.name, () => {
|
||||||
.attach('assetData', assetData, filename)
|
.attach('assetData', assetData, filename)
|
||||||
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
|
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(factory.responses.badRequest(['isFavorite must be a boolean value']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if `visibility` is not an enum', async () => {
|
it('should throw if `visibility` is not an enum', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer())
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
.post('/assets')
|
.post('/assets')
|
||||||
.attach('assetData', assetData, filename)
|
.attach('assetData', assetData, filename)
|
||||||
.field({ ...makeUploadDto(), visibility: 'not-a-boolean' });
|
.field({ ...makeUploadDto(), visibility: 'not-an-option' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(
|
||||||
|
factory.responses.badRequest([expect.stringContaining('visibility must be one of the following values:')]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO figure out how to deal with `sendFile`
|
// TODO figure out how to deal with `sendFile`
|
||||||
|
|
|
||||||
|
|
@ -128,12 +128,6 @@ describe(SearchController.name, () => {
|
||||||
await request(ctx.getHttpServer()).post('/search/smart');
|
await request(ctx.getHttpServer()).post('/search/smart');
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a query', async () => {
|
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /search/explore', () => {
|
describe('GET /search/explore', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { plainToInstance, Transform, Type } from 'class-transformer';
|
||||||
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||||
import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto';
|
import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto';
|
||||||
import { AssetVisibility } from 'src/enum';
|
import { AssetVisibility } from 'src/enum';
|
||||||
|
|
@ -65,10 +66,18 @@ export class AssetMediaCreateDto extends AssetMediaBase {
|
||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true })
|
||||||
livePhotoVideoId?: string;
|
livePhotoVideoId?: string;
|
||||||
|
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(value);
|
||||||
|
const items = Array.isArray(json) ? json : [json];
|
||||||
|
return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item));
|
||||||
|
} catch {
|
||||||
|
throw new BadRequestException(['metadata must be valid JSON']);
|
||||||
|
}
|
||||||
|
})
|
||||||
@Optional()
|
@Optional()
|
||||||
@IsArray()
|
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => AssetMetadataUpsertItemDto)
|
@IsArray()
|
||||||
metadata!: AssetMetadataUpsertItemDto[];
|
metadata!: AssetMetadataUpsertItemDto[];
|
||||||
|
|
||||||
@ApiProperty({ type: 'string', format: 'binary', required: false })
|
@ApiProperty({ type: 'string', format: 'binary', required: false })
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,12 @@ export class StatisticsSearchDto extends BaseSearchDto {
|
||||||
export class SmartSearchDto extends BaseSearchWithResultsDto {
|
export class SmartSearchDto extends BaseSearchWithResultsDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
query!: string;
|
@Optional()
|
||||||
|
query?: string;
|
||||||
|
|
||||||
|
@ValidateUUID({ optional: true })
|
||||||
|
@Optional()
|
||||||
|
queryAssetId?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,9 @@ export class SyncAckV1 {}
|
||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
export class SyncResetV1 {}
|
export class SyncResetV1 {}
|
||||||
|
|
||||||
|
@ExtraModel()
|
||||||
|
export class SyncCompleteV1 {}
|
||||||
|
|
||||||
export type SyncItem = {
|
export type SyncItem = {
|
||||||
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
|
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
|
||||||
[SyncEntityType.UserV1]: SyncUserV1;
|
[SyncEntityType.UserV1]: SyncUserV1;
|
||||||
|
|
@ -382,6 +385,7 @@ export type SyncItem = {
|
||||||
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
||||||
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
||||||
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||||
|
[SyncEntityType.SyncCompleteV1]: SyncCompleteV1;
|
||||||
[SyncEntityType.SyncResetV1]: SyncResetV1;
|
[SyncEntityType.SyncResetV1]: SyncResetV1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,7 @@ export enum JobName {
|
||||||
AssetGenerateThumbnails = 'AssetGenerateThumbnails',
|
AssetGenerateThumbnails = 'AssetGenerateThumbnails',
|
||||||
|
|
||||||
AuditLogCleanup = 'AuditLogCleanup',
|
AuditLogCleanup = 'AuditLogCleanup',
|
||||||
|
AuditTableCleanup = 'AuditTableCleanup',
|
||||||
|
|
||||||
DatabaseBackup = 'DatabaseBackup',
|
DatabaseBackup = 'DatabaseBackup',
|
||||||
|
|
||||||
|
|
@ -570,8 +571,7 @@ export enum JobName {
|
||||||
SendMail = 'SendMail',
|
SendMail = 'SendMail',
|
||||||
|
|
||||||
SidecarQueueAll = 'SidecarQueueAll',
|
SidecarQueueAll = 'SidecarQueueAll',
|
||||||
SidecarDiscovery = 'SidecarDiscovery',
|
SidecarCheck = 'SidecarCheck',
|
||||||
SidecarSync = 'SidecarSync',
|
|
||||||
SidecarWrite = 'SidecarWrite',
|
SidecarWrite = 'SidecarWrite',
|
||||||
|
|
||||||
SmartSearchQueueAll = 'SmartSearchQueueAll',
|
SmartSearchQueueAll = 'SmartSearchQueueAll',
|
||||||
|
|
@ -708,6 +708,7 @@ export enum SyncEntityType {
|
||||||
|
|
||||||
SyncAckV1 = 'SyncAckV1',
|
SyncAckV1 = 'SyncAckV1',
|
||||||
SyncResetV1 = 'SyncResetV1',
|
SyncResetV1 = 'SyncResetV1',
|
||||||
|
SyncCompleteV1 = 'SyncCompleteV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationLevel {
|
export enum NotificationLevel {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,18 @@ where
|
||||||
limit
|
limit
|
||||||
$2
|
$2
|
||||||
|
|
||||||
|
-- AssetJobRepository.getForSidecarCheckJob
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"sidecarPath",
|
||||||
|
"originalPath"
|
||||||
|
from
|
||||||
|
"asset"
|
||||||
|
where
|
||||||
|
"asset"."id" = $1::uuid
|
||||||
|
limit
|
||||||
|
$2
|
||||||
|
|
||||||
-- AssetJobRepository.streamForThumbnailJob
|
-- AssetJobRepository.streamForThumbnailJob
|
||||||
select
|
select
|
||||||
"asset"."id",
|
"asset"."id",
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,14 @@ offset
|
||||||
$8
|
$8
|
||||||
commit
|
commit
|
||||||
|
|
||||||
|
-- SearchRepository.getEmbedding
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
"smart_search"
|
||||||
|
where
|
||||||
|
"assetId" = $1
|
||||||
|
|
||||||
-- SearchRepository.searchFaces
|
-- SearchRepository.searchFaces
|
||||||
begin
|
begin
|
||||||
set
|
set
|
||||||
|
|
|
||||||
|
|
@ -957,7 +957,7 @@ where
|
||||||
order by
|
order by
|
||||||
"stack"."updateId" asc
|
"stack"."updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.people.getDeletes
|
-- SyncRepository.person.getDeletes
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
"personId"
|
"personId"
|
||||||
|
|
@ -970,7 +970,7 @@ where
|
||||||
order by
|
order by
|
||||||
"person_audit"."id" asc
|
"person_audit"."id" asc
|
||||||
|
|
||||||
-- SyncRepository.people.getUpserts
|
-- SyncRepository.person.getUpserts
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,8 @@ export class AssetJobRepository {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.where('asset.id', '=', asUuid(id))
|
.where('asset.id', '=', asUuid(id))
|
||||||
.select((eb) => [
|
.select(['id', 'sidecarPath', 'originalPath'])
|
||||||
'id',
|
.select((eb) =>
|
||||||
'sidecarPath',
|
|
||||||
'originalPath',
|
|
||||||
jsonArrayFrom(
|
jsonArrayFrom(
|
||||||
eb
|
eb
|
||||||
.selectFrom('tag')
|
.selectFrom('tag')
|
||||||
|
|
@ -50,7 +48,17 @@ export class AssetJobRepository {
|
||||||
.innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId')
|
.innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId')
|
||||||
.whereRef('asset.id', '=', 'tag_asset.assetsId'),
|
.whereRef('asset.id', '=', 'tag_asset.assetsId'),
|
||||||
).as('tags'),
|
).as('tags'),
|
||||||
])
|
)
|
||||||
|
.limit(1)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
getForSidecarCheckJob(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('asset')
|
||||||
|
.where('asset.id', '=', asUuid(id))
|
||||||
|
.select(['id', 'sidecarPath', 'originalPath'])
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ export class MediaRepository {
|
||||||
failOn: options.processInvalidImages ? 'none' : 'error',
|
failOn: options.processInvalidImages ? 'none' : 'error',
|
||||||
limitInputPixels: false,
|
limitInputPixels: false,
|
||||||
raw: options.raw,
|
raw: options.raw,
|
||||||
|
unlimited: true,
|
||||||
})
|
})
|
||||||
.pipelineColorspace(options.colorspace === Colorspace.Srgb ? 'srgb' : 'rgb16')
|
.pipelineColorspace(options.colorspace === Colorspace.Srgb ? 'srgb' : 'rgb16')
|
||||||
.withIccProfile(options.colorspace);
|
.withIccProfile(options.colorspace);
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,13 @@ export class SearchRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({
|
||||||
|
params: [DummyValue.UUID],
|
||||||
|
})
|
||||||
|
async getEmbedding(assetId: string) {
|
||||||
|
return this.db.selectFrom('smart_search').selectAll().where('assetId', '=', assetId).executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely, sql } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
|
|
@ -62,7 +62,7 @@ export class SyncRepository {
|
||||||
partnerAsset: PartnerAssetsSync;
|
partnerAsset: PartnerAssetsSync;
|
||||||
partnerAssetExif: PartnerAssetExifsSync;
|
partnerAssetExif: PartnerAssetExifsSync;
|
||||||
partnerStack: PartnerStackSync;
|
partnerStack: PartnerStackSync;
|
||||||
people: PersonSync;
|
person: PersonSync;
|
||||||
stack: StackSync;
|
stack: StackSync;
|
||||||
user: UserSync;
|
user: UserSync;
|
||||||
userMetadata: UserMetadataSync;
|
userMetadata: UserMetadataSync;
|
||||||
|
|
@ -84,7 +84,7 @@ export class SyncRepository {
|
||||||
this.partnerAsset = new PartnerAssetsSync(this.db);
|
this.partnerAsset = new PartnerAssetsSync(this.db);
|
||||||
this.partnerAssetExif = new PartnerAssetExifsSync(this.db);
|
this.partnerAssetExif = new PartnerAssetExifsSync(this.db);
|
||||||
this.partnerStack = new PartnerStackSync(this.db);
|
this.partnerStack = new PartnerStackSync(this.db);
|
||||||
this.people = new PersonSync(this.db);
|
this.person = new PersonSync(this.db);
|
||||||
this.stack = new StackSync(this.db);
|
this.stack = new StackSync(this.db);
|
||||||
this.user = new UserSync(this.db);
|
this.user = new UserSync(this.db);
|
||||||
this.userMetadata = new UserMetadataSync(this.db);
|
this.userMetadata = new UserMetadataSync(this.db);
|
||||||
|
|
@ -117,6 +117,15 @@ class BaseSync {
|
||||||
.orderBy(idRef, 'asc');
|
.orderBy(idRef, 'asc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected auditCleanup<T extends keyof DB>(t: T, days: number) {
|
||||||
|
const { table, ref } = this.db.dynamic;
|
||||||
|
|
||||||
|
return this.db
|
||||||
|
.deleteFrom(table(t).as(t))
|
||||||
|
.where(ref(`${t}.deletedAt`), '<', sql.raw(`now() - interval '${days} days'`))
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
protected upsertQuery<T extends keyof DB>(t: T, { nowId, ack }: SyncQueryOptions) {
|
protected upsertQuery<T extends keyof DB>(t: T, { nowId, ack }: SyncQueryOptions) {
|
||||||
const { table, ref } = this.db.dynamic;
|
const { table, ref } = this.db.dynamic;
|
||||||
const updateIdRef = ref(`${t}.updateId`);
|
const updateIdRef = ref(`${t}.updateId`);
|
||||||
|
|
@ -150,6 +159,10 @@ class AlbumSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('album_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
|
|
@ -286,6 +299,10 @@ class AlbumToAssetSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('album_asset_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
|
|
@ -334,6 +351,10 @@ class AlbumUserSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('album_user_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
|
|
@ -371,6 +392,10 @@ class AssetSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('asset_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('asset', options)
|
return this.upsertQuery('asset', options)
|
||||||
|
|
@ -400,6 +425,10 @@ class PersonSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('person_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('person', options)
|
return this.upsertQuery('person', options)
|
||||||
|
|
@ -431,6 +460,10 @@ class AssetFaceSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('asset_face_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('asset_face', options)
|
return this.upsertQuery('asset_face', options)
|
||||||
|
|
@ -473,6 +506,10 @@ class MemorySync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('memory_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('memory', options)
|
return this.upsertQuery('memory', options)
|
||||||
|
|
@ -505,6 +542,10 @@ class MemoryToAssetSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('memory_asset_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('memory_asset', options)
|
return this.upsertQuery('memory_asset', options)
|
||||||
|
|
@ -537,6 +578,10 @@ class PartnerSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('partner_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
|
|
@ -616,6 +661,10 @@ class StackSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('stack_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('stack', options)
|
return this.upsertQuery('stack', options)
|
||||||
|
|
@ -664,6 +713,10 @@ class UserSync extends BaseSync {
|
||||||
return this.auditQuery('user_audit', options).select(['id', 'userId']).stream();
|
return this.auditQuery('user_audit', options).select(['id', 'userId']).stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('user_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('user', options).select(columns.syncUser).stream();
|
return this.upsertQuery('user', options).select(columns.syncUser).stream();
|
||||||
|
|
@ -679,6 +732,10 @@ class UserMetadataSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('user_metadata_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('user_metadata', options)
|
return this.upsertQuery('user_metadata', options)
|
||||||
|
|
@ -698,6 +755,10 @@ class AssetMetadataSync extends BaseSync {
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('asset_metadata_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions, userId: string) {
|
getUpserts(options: SyncQueryOptions, userId: string) {
|
||||||
return this.upsertQuery('asset_metadata', options)
|
return this.upsertQuery('asset_metadata', options)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue