Compare commits
32 Commits
v1.139.2
...
test-fix-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da7fbcbb46 | ||
|
|
b471e190a0 | ||
|
|
7672c8c6e0 | ||
|
|
386a6bb377 | ||
|
|
63088b22e0 | ||
|
|
d9d8beb92f | ||
|
|
38a8a67be9 | ||
|
|
7531ffcbfb | ||
|
|
d5f3629c49 | ||
|
|
be5b4cb1d1 | ||
|
|
5fb8d651ec | ||
|
|
c2313f7a99 | ||
|
|
59627e2b4c | ||
|
|
530bf059ad | ||
|
|
b44d2a241d | ||
|
|
1af10ded74 | ||
|
|
3f1e11afcc | ||
|
|
28dce2d0df | ||
|
|
605764f226 | ||
|
|
44e1c83c84 | ||
|
|
0729887c9c | ||
|
|
3bfa8b7575 | ||
|
|
3138048b96 | ||
|
|
f8b41ea8aa | ||
|
|
1d33ed6bed | ||
|
|
2be1a58c5b | ||
|
|
03e7922589 | ||
|
|
801af34d9a | ||
|
|
bedaa729e9 | ||
|
|
13c8a6e61d | ||
|
|
01edf6533b | ||
|
|
30d0bea4df |
@@ -5,7 +5,8 @@
|
|||||||
"immich-server",
|
"immich-server",
|
||||||
"redis",
|
"redis",
|
||||||
"database",
|
"database",
|
||||||
"immich-machine-learning"
|
"immich-machine-learning",
|
||||||
|
"init"
|
||||||
],
|
],
|
||||||
"dockerComposeFile": [
|
"dockerComposeFile": [
|
||||||
"../docker/docker-compose.dev.yml",
|
"../docker/docker-compose.dev.yml",
|
||||||
|
|||||||
@@ -11,8 +11,23 @@ services:
|
|||||||
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
|
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
|
- docs-node_modules:/usr/src/app/docs/node_modules
|
||||||
|
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
||||||
|
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
||||||
|
- app-node_modules:/usr/src/app/node_modules
|
||||||
|
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||||
|
- coverage:/usr/src/app/web/coverage
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
init:
|
||||||
|
env_file: !reset []
|
||||||
|
command: sh -c 'for path in /data /data/upload /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
database:
|
database:
|
||||||
|
|||||||
@@ -4,34 +4,13 @@ module.exports = {
|
|||||||
if (!pkg.name) {
|
if (!pkg.name) {
|
||||||
return pkg;
|
return pkg;
|
||||||
}
|
}
|
||||||
switch (pkg.name) {
|
if (pkg.name === "exiftool-vendored") {
|
||||||
case "exiftool-vendored":
|
if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
|
||||||
if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
|
// make exiftool-vendored.pl a regular dependency
|
||||||
// make exiftool-vendored.pl a regular dependency
|
pkg.dependencies["exiftool-vendored.pl"] =
|
||||||
pkg.dependencies["exiftool-vendored.pl"] =
|
pkg.optionalDependencies["exiftool-vendored.pl"];
|
||||||
pkg.optionalDependencies["exiftool-vendored.pl"];
|
delete pkg.optionalDependencies["exiftool-vendored.pl"];
|
||||||
delete pkg.optionalDependencies["exiftool-vendored.pl"];
|
}
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "sharp":
|
|
||||||
const optionalDeps = Object.keys(pkg.optionalDependencies).filter(
|
|
||||||
(dep) => dep.startsWith("@img")
|
|
||||||
);
|
|
||||||
for (const dep of optionalDeps) {
|
|
||||||
// remove all optionalDependencies from sharp (they will be compiled from source), except:
|
|
||||||
// include the precompiled musl version of sharp, for web
|
|
||||||
// include precompiled linux-x64 version of sharp, for server (stage: web-prod)
|
|
||||||
// include precompiled linux-arm64 version of sharp, for server (stage: web-prod)
|
|
||||||
if (
|
|
||||||
dep.includes("musl") ||
|
|
||||||
dep.includes("linux-x64") ||
|
|
||||||
dep.includes("linux-arm64")
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
delete pkg.optionalDependencies[dep];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return pkg;
|
return pkg;
|
||||||
},
|
},
|
||||||
|
|||||||
48
Makefile
48
Makefile
@@ -1,29 +1,29 @@
|
|||||||
dev:
|
dev: prepare-volumes
|
||||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-down:
|
dev-down:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update: prepare-volumes
|
||||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-scale:
|
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:
|
dev-docs: prepare-volumes
|
||||||
npm --prefix docs run start
|
npm --prefix docs run start
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e: prepare-volumes
|
||||||
@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 --remove-orphans
|
||||||
|
|
||||||
e2e-update:
|
e2e-update: prepare-volumes
|
||||||
@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:
|
||||||
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-down:
|
prod-down:
|
||||||
@@ -33,16 +33,16 @@ prod-scale:
|
|||||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
.PHONY: open-api
|
.PHONY: open-api
|
||||||
open-api:
|
open-api: prepare-volumes
|
||||||
cd ./open-api && bash ./bin/generate-open-api.sh
|
cd ./open-api && bash ./bin/generate-open-api.sh
|
||||||
|
|
||||||
open-api-dart:
|
open-api-dart: prepare-volumes
|
||||||
cd ./open-api && bash ./bin/generate-open-api.sh dart
|
cd ./open-api && bash ./bin/generate-open-api.sh dart
|
||||||
|
|
||||||
open-api-typescript:
|
open-api-typescript: prepare-volumes
|
||||||
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
||||||
|
|
||||||
sql:
|
sql: prepare-volumes
|
||||||
pnpm --filter immich run sync:sql
|
pnpm --filter immich run sync:sql
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
@@ -51,6 +51,30 @@ attach-server:
|
|||||||
renovate:
|
renovate:
|
||||||
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
|
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
|
||||||
|
|
||||||
|
# Directories that need to be created for volumes or build output
|
||||||
|
VOLUME_DIRS = \
|
||||||
|
./.pnpm-store \
|
||||||
|
./web/.svelte-kit \
|
||||||
|
./web/node_modules \
|
||||||
|
./web/coverage \
|
||||||
|
./e2e/node_modules \
|
||||||
|
./docs/node_modules \
|
||||||
|
./server/node_modules \
|
||||||
|
./server/dist \
|
||||||
|
./open-api/typescript-sdk/node_modules \
|
||||||
|
./.github/node_modules \
|
||||||
|
./node_modules \
|
||||||
|
./cli/node_modules
|
||||||
|
|
||||||
|
# create empty directories and chown to current user
|
||||||
|
prepare-volumes:
|
||||||
|
@for dir in $(VOLUME_DIRS); do \
|
||||||
|
mkdir -p $$dir; \
|
||||||
|
done
|
||||||
|
@if [ -n "$(VOLUME_DIRS)" ]; then \
|
||||||
|
chown -R $$(id -u):$$(id -g) $(VOLUME_DIRS); \
|
||||||
|
fi
|
||||||
|
|
||||||
MODULES = e2e server web cli sdk docs .github
|
MODULES = e2e server web cli sdk docs .github
|
||||||
|
|
||||||
# directory to package name mapping function
|
# directory to package name mapping function
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.82",
|
"version": "2.2.84",
|
||||||
"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",
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/photos:/data
|
- ${UPLOAD_LOCATION}/photos:/data
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/data/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/data/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
|
- docs-node_modules:/usr/src/app/docs/node_modules
|
||||||
|
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
||||||
|
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
||||||
|
- app-node_modules:/usr/src/app/node_modules
|
||||||
|
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||||
|
- coverage:/usr/src/app/web/coverage
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -84,6 +96,18 @@ services:
|
|||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/usr/src/app
|
- ..:/usr/src/app
|
||||||
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
|
- docs-node_modules:/usr/src/app/docs/node_modules
|
||||||
|
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
||||||
|
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
||||||
|
- app-node_modules:/usr/src/app/node_modules
|
||||||
|
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||||
|
- coverage:/usr/src/app/web/coverage
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 1048576
|
soft: 1048576
|
||||||
@@ -167,9 +191,33 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
user: 0:0
|
user: 0:0
|
||||||
command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
|
command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
|
||||||
|
volumes:
|
||||||
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
|
- docs-node_modules:/usr/src/app/docs/node_modules
|
||||||
|
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
||||||
|
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
||||||
|
- app-node_modules:/usr/src/app/node_modules
|
||||||
|
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||||
|
- coverage:/usr/src/app/web/coverage
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
prometheus-data:
|
prometheus-data:
|
||||||
grafana-data:
|
grafana-data:
|
||||||
|
pnpm-store:
|
||||||
|
server-node-modules:
|
||||||
|
server-dist:
|
||||||
|
web-node_modules:
|
||||||
|
github-node_modules:
|
||||||
|
cli-node_modules:
|
||||||
|
docs-node_modules:
|
||||||
|
e2e-node_modules:
|
||||||
|
sdk-node_modules:
|
||||||
|
app-node_modules:
|
||||||
|
sveltekit:
|
||||||
|
coverage:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Unable to set `app.immich:///oauth-callback` as a valid redirect URI? See [Mobil
|
|||||||
|
|
||||||
Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an identity layer built on top of OAuth2. OIDC is supported by most identity providers, including:
|
Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an identity layer built on top of OAuth2. OIDC is supported by most identity providers, including:
|
||||||
|
|
||||||
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
|
- [Authentik](https://integrations.goauthentik.io/media/immich/)
|
||||||
- [Authelia](https://www.authelia.com/integration/openid-connect/immich/)
|
- [Authelia](https://www.authelia.com/integration/openid-connect/immich/)
|
||||||
- [Okta](https://www.okta.com/openid-connect/)
|
- [Okta](https://www.okta.com/openid-connect/)
|
||||||
- [Google](https://developers.google.com/identity/openid-connect/openid-connect)
|
- [Google](https://developers.google.com/identity/openid-connect/openid-connect)
|
||||||
|
|||||||
8
docs/static/archived-versions.json
vendored
8
docs/static/archived-versions.json
vendored
@@ -1,4 +1,12 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.139.4",
|
||||||
|
"url": "https://v1.139.4.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.139.3",
|
||||||
|
"url": "https://v1.139.3.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.139.2",
|
"label": "v1.139.2",
|
||||||
"url": "https://v1.139.2.archive.immich.app"
|
"url": "https://v1.139.2.archive.immich.app"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.139.2",
|
"version": "1.139.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -500,7 +500,7 @@
|
|||||||
"assets": "Assets",
|
"assets": "Assets",
|
||||||
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
|
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
|
||||||
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
||||||
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal} albums",
|
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}",
|
||||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
||||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
||||||
"assets_count": "{count, plural, one {# asset} other {# assets}}",
|
"assets_count": "{count, plural, one {# asset} other {# assets}}",
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 3007,
|
"android.injected.version.code" => 3009,
|
||||||
"android.injected.version.name" => "1.139.2",
|
"android.injected.version.name" => "1.139.4",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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')
|
||||||
|
|||||||
@@ -123,6 +123,8 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -667,7 +669,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -811,7 +813,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -841,7 +843,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -875,7 +877,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -918,7 +920,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -958,7 +960,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -997,7 +999,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1041,7 +1043,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1082,7 +1084,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 215;
|
CURRENT_PROJECT_VERSION = 217;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.138.1</string>
|
<string>1.139.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>215</string>
|
<string>217</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ platform :ios do
|
|||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
)
|
)
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.139.2"
|
version_number: "1.139.4"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ isOnboarded: $isOnboarded,
|
|||||||
int get hashCode => isOnboarded.hashCode;
|
int get hashCode => isOnboarded.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: wait to be overwritten
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
final bool foldersEnabled;
|
final bool foldersEnabled;
|
||||||
final bool memoriesEnabled;
|
final bool memoriesEnabled;
|
||||||
@@ -133,17 +132,17 @@ class Preferences {
|
|||||||
|
|
||||||
factory Preferences.fromMap(Map<String, Object?> map) {
|
factory Preferences.fromMap(Map<String, Object?> map) {
|
||||||
return Preferences(
|
return Preferences(
|
||||||
foldersEnabled: map["folders-Enabled"] as bool? ?? false,
|
foldersEnabled: (map["folders"] as Map<String, Object?>?)?["enabled"] as bool? ?? false,
|
||||||
memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
|
memoriesEnabled: (map["memories"] as Map<String, Object?>?)?["enabled"] as bool? ?? true,
|
||||||
peopleEnabled: map["people-Enabled"] as bool? ?? true,
|
peopleEnabled: (map["people"] as Map<String, Object?>?)?["enabled"] as bool? ?? true,
|
||||||
ratingsEnabled: map["ratings-Enabled"] as bool? ?? false,
|
ratingsEnabled: (map["ratings"] as Map<String, Object?>?)?["enabled"] as bool? ?? false,
|
||||||
sharedLinksEnabled: map["sharedLinks-Enabled"] as bool? ?? true,
|
sharedLinksEnabled: (map["sharedLinks"] as Map<String, Object?>?)?["enabled"] as bool? ?? true,
|
||||||
tagsEnabled: map["tags-Enabled"] as bool? ?? false,
|
tagsEnabled: (map["tags"] as Map<String, Object?>?)?["enabled"] as bool? ?? false,
|
||||||
userAvatarColor: AvatarColor.values.firstWhere(
|
userAvatarColor: AvatarColor.values.firstWhere(
|
||||||
(e) => e.value == map["avatar-Color"] as String?,
|
(e) => e.value == (map["avatar"] as Map<String, Object?>?)?["color"] as String?,
|
||||||
orElse: () => AvatarColor.primary,
|
orElse: () => AvatarColor.primary,
|
||||||
),
|
),
|
||||||
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
|
showSupportBadge: (map["purchase"] as Map<String, Object?>?)?["showSupportBadge"] as bool? ?? true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ class License {
|
|||||||
|
|
||||||
factory License.fromMap(Map<String, Object?> map) {
|
factory License.fromMap(Map<String, Object?> map) {
|
||||||
return License(
|
return License(
|
||||||
activatedAt: map["activatedAt"] as DateTime,
|
activatedAt: DateTime.parse(map["activatedAt"] as String),
|
||||||
activationKey: map["activationKey"] as String,
|
activationKey: map["activationKey"] as String,
|
||||||
licenseKey: map["licenseKey"] as String,
|
licenseKey: map["licenseKey"] as String,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,9 +17,14 @@ class AssetService {
|
|||||||
_localAssetRepository = localAssetRepository,
|
_localAssetRepository = localAssetRepository,
|
||||||
_platform = const LocalPlatform();
|
_platform = const LocalPlatform();
|
||||||
|
|
||||||
|
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
||||||
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||||
|
return asset is LocalAsset ? _localAssetRepository.get(id) : _remoteAssetRepository.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
Stream<BaseAsset?> watchAsset(BaseAsset asset) {
|
Stream<BaseAsset?> watchAsset(BaseAsset asset) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||||
return asset is LocalAsset ? _localAssetRepository.watchAsset(id) : _remoteAssetRepository.watchAsset(id);
|
return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<RemoteAsset?> getRemoteAsset(String id) {
|
Future<RemoteAsset?> getRemoteAsset(String id) {
|
||||||
|
|||||||
@@ -64,18 +64,48 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final bytes = Uint8List(response.contentLength);
|
|
||||||
|
// Handle unknown content length from reverse proxy
|
||||||
|
final contentLength = response.contentLength;
|
||||||
|
final Uint8List bytes;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
final subscription = response.listen((List<int> chunk) {
|
|
||||||
// this is important to break the response stream if the request is cancelled
|
if (contentLength >= 0) {
|
||||||
if (_isCancelled) {
|
// Known content length - use pre-allocated buffer
|
||||||
throw StateError('Cancelled request');
|
bytes = Uint8List(contentLength);
|
||||||
|
final subscription = response.listen((List<int> chunk) {
|
||||||
|
// this is important to break the response stream if the request is cancelled
|
||||||
|
if (_isCancelled) {
|
||||||
|
throw StateError('Cancelled request');
|
||||||
|
}
|
||||||
|
bytes.setAll(offset, chunk);
|
||||||
|
offset += chunk.length;
|
||||||
|
}, cancelOnError: true);
|
||||||
|
cacheManager?.putStreamedFile(url, response);
|
||||||
|
await subscription.asFuture();
|
||||||
|
} else {
|
||||||
|
// Unknown content length - collect chunks dynamically
|
||||||
|
final chunks = <List<int>>[];
|
||||||
|
int totalLength = 0;
|
||||||
|
final subscription = response.listen((List<int> chunk) {
|
||||||
|
// this is important to break the response stream if the request is cancelled
|
||||||
|
if (_isCancelled) {
|
||||||
|
throw StateError('Cancelled request');
|
||||||
|
}
|
||||||
|
chunks.add(chunk);
|
||||||
|
totalLength += chunk.length;
|
||||||
|
}, cancelOnError: true);
|
||||||
|
cacheManager?.putStreamedFile(url, response);
|
||||||
|
await subscription.asFuture();
|
||||||
|
|
||||||
|
// Combine all chunks into a single buffer
|
||||||
|
bytes = Uint8List(totalLength);
|
||||||
|
for (final chunk in chunks) {
|
||||||
|
bytes.setAll(offset, chunk);
|
||||||
|
offset += chunk.length;
|
||||||
}
|
}
|
||||||
bytes.setAll(offset, chunk);
|
}
|
||||||
offset += chunk.length;
|
|
||||||
}, cancelOnError: true);
|
|
||||||
cacheManager?.putStreamedFile(url, response);
|
|
||||||
await subscription.asFuture();
|
|
||||||
return await ImmutableBuffer.fromUint8List(bytes);
|
return await ImmutableBuffer.fromUint8List(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
final Drift _db;
|
final Drift _db;
|
||||||
const DriftLocalAssetRepository(this._db) : super(_db);
|
const DriftLocalAssetRepository(this._db) : super(_db);
|
||||||
|
|
||||||
Stream<LocalAsset?> watchAsset(String id) {
|
SingleOrNullSelectable<LocalAsset?> _assetSelectable(String id) {
|
||||||
final query = _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([
|
final query = _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([
|
||||||
leftOuterJoin(
|
leftOuterJoin(
|
||||||
_db.remoteAssetEntity,
|
_db.remoteAssetEntity,
|
||||||
@@ -21,9 +21,13 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
final asset = row.readTable(_db.localAssetEntity).toDto();
|
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||||
return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id));
|
return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id));
|
||||||
}).watchSingleOrNull();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> get(String id) => _assetSelectable(id).getSingleOrNull();
|
||||||
|
|
||||||
|
Stream<LocalAsset?> watch(String id) => _assetSelectable(id).watchSingleOrNull();
|
||||||
|
|
||||||
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
|
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
|
||||||
if (hashes.isEmpty) {
|
if (hashes.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {SortRemoteAlbumsBy.updatedAt}}) {
|
Future<List<RemoteAlbum>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {SortRemoteAlbumsBy.updatedAt}}) {
|
||||||
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true);
|
||||||
|
|
||||||
final query = _db.remoteAlbumEntity.select().join([
|
final query = _db.remoteAlbumEntity.select().join([
|
||||||
leftOuterJoin(
|
leftOuterJoin(
|
||||||
@@ -41,7 +41,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
..where(_db.remoteAssetEntity.deletedAt.isNull())
|
..where(_db.remoteAssetEntity.deletedAt.isNull())
|
||||||
..addColumns([assetCount])
|
..addColumns([assetCount])
|
||||||
..addColumns([_db.userEntity.name])
|
..addColumns([_db.userEntity.name])
|
||||||
..addColumns([_db.remoteAlbumUserEntity.userId.count()])
|
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
|
||||||
..groupBy([_db.remoteAlbumEntity.id]);
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
if (sortBy.isNotEmpty) {
|
if (sortBy.isNotEmpty) {
|
||||||
@@ -62,14 +62,14 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
.toDto(
|
.toDto(
|
||||||
assetCount: row.read(assetCount) ?? 0,
|
assetCount: row.read(assetCount) ?? 0,
|
||||||
ownerName: row.read(_db.userEntity.name)!,
|
ownerName: row.read(_db.userEntity.name)!,
|
||||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2,
|
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<RemoteAlbum?> get(String albumId) {
|
Future<RemoteAlbum?> get(String albumId) {
|
||||||
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true);
|
||||||
|
|
||||||
final query =
|
final query =
|
||||||
_db.remoteAlbumEntity.select().join([
|
_db.remoteAlbumEntity.select().join([
|
||||||
@@ -97,7 +97,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull())
|
..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull())
|
||||||
..addColumns([assetCount])
|
..addColumns([assetCount])
|
||||||
..addColumns([_db.userEntity.name])
|
..addColumns([_db.userEntity.name])
|
||||||
..addColumns([_db.remoteAlbumUserEntity.userId.count()])
|
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
|
||||||
..groupBy([_db.remoteAlbumEntity.id]);
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
return query
|
return query
|
||||||
@@ -107,7 +107,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
.toDto(
|
.toDto(
|
||||||
assetCount: row.read(assetCount) ?? 0,
|
assetCount: row.read(assetCount) ?? 0,
|
||||||
ownerName: row.read(_db.userEntity.name)!,
|
ownerName: row.read(_db.userEntity.name)!,
|
||||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2,
|
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
@@ -282,7 +282,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
])
|
])
|
||||||
..where(_db.remoteAlbumEntity.id.equals(albumId))
|
..where(_db.remoteAlbumEntity.id.equals(albumId))
|
||||||
..addColumns([_db.userEntity.name])
|
..addColumns([_db.userEntity.name])
|
||||||
..addColumns([_db.remoteAlbumUserEntity.userId.count()])
|
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
|
||||||
..groupBy([_db.remoteAlbumEntity.id]);
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
@@ -290,7 +290,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
.readTable(_db.remoteAlbumEntity)
|
.readTable(_db.remoteAlbumEntity)
|
||||||
.toDto(
|
.toDto(
|
||||||
ownerName: row.read(_db.userEntity.name)!,
|
ownerName: row.read(_db.userEntity.name)!,
|
||||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2,
|
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
|
||||||
);
|
);
|
||||||
return album;
|
return album;
|
||||||
}).watchSingleOrNull();
|
}).watchSingleOrNull();
|
||||||
|
|||||||
@@ -55,24 +55,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
return _assetSelectable(id).getSingleOrNull();
|
return _assetSelectable(id).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<RemoteAsset?> watchAsset(String id) {
|
|
||||||
final query =
|
|
||||||
_db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([
|
|
||||||
leftOuterJoin(
|
|
||||||
_db.localAssetEntity,
|
|
||||||
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
..where(_db.remoteAssetEntity.id.equals(id))
|
|
||||||
..limit(1);
|
|
||||||
|
|
||||||
return query.map((row) {
|
|
||||||
final asset = row.readTable(_db.remoteAssetEntity).toDto();
|
|
||||||
return asset.copyWith(localId: row.read(_db.localAssetEntity.id));
|
|
||||||
}).watchSingleOrNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
||||||
if (asset.stackId == null) {
|
if (asset.stackId == null) {
|
||||||
return Future.value([]);
|
return Future.value([]);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.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/infrastructure/repositories/user_metadata.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class IsarUserRepository extends IsarDatabaseRepository {
|
class IsarUserRepository extends IsarDatabaseRepository {
|
||||||
@@ -70,8 +72,16 @@ class DriftUserRepository extends DriftDatabaseRepository {
|
|||||||
final Drift _db;
|
final Drift _db;
|
||||||
const DriftUserRepository(super.db) : _db = db;
|
const DriftUserRepository(super.db) : _db = db;
|
||||||
|
|
||||||
Future<UserDto?> get(String id) =>
|
Future<UserDto?> get(String id) async {
|
||||||
_db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull().then((user) => user?.toDto());
|
final user = await _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
|
||||||
|
|
||||||
|
if (user == null) return null;
|
||||||
|
|
||||||
|
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(id));
|
||||||
|
final metadata = await query.map((row) => row.toDto()).get();
|
||||||
|
|
||||||
|
return user.toDto(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
Future<UserDto> upsert(UserDto user) async {
|
Future<UserDto> upsert(UserDto user) async {
|
||||||
await _db.userEntity.insertOnConflictUpdate(
|
await _db.userEntity.insertOnConflictUpdate(
|
||||||
@@ -87,10 +97,35 @@ class DriftUserRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<UserDto>> getAll() async {
|
||||||
|
final users = await _db.userEntity.select().get();
|
||||||
|
final List<UserDto> result = [];
|
||||||
|
|
||||||
|
for (final user in users) {
|
||||||
|
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id));
|
||||||
|
final metadata = await query.map((row) => row.toDto()).get();
|
||||||
|
result.add(user.toDto(metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on UserEntityData {
|
extension on UserEntityData {
|
||||||
UserDto toDto() {
|
UserDto toDto([List<UserMetadata>? metadata]) {
|
||||||
|
AvatarColor avatarColor = AvatarColor.primary;
|
||||||
|
bool memoryEnabled = true;
|
||||||
|
|
||||||
|
if (metadata != null) {
|
||||||
|
for (final meta in metadata) {
|
||||||
|
if (meta.key == UserMetadataKey.preferences && meta.preferences != null) {
|
||||||
|
avatarColor = meta.preferences?.userAvatarColor ?? AvatarColor.primary;
|
||||||
|
memoryEnabled = meta.preferences?.memoriesEnabled ?? true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return UserDto(
|
return UserDto(
|
||||||
id: id,
|
id: id,
|
||||||
email: email,
|
email: email,
|
||||||
@@ -99,6 +134,8 @@ extension on UserEntityData {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
profileChangedAt: profileChangedAt,
|
profileChangedAt: profileChangedAt,
|
||||||
hasProfileImage: hasProfileImage,
|
hasProfileImage: hasProfileImage,
|
||||||
|
avatarColor: avatarColor,
|
||||||
|
memoryEnabled: memoryEnabled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class DriftUserMetadataRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on UserMetadataEntityData {
|
extension UserMetadataDataExtension on UserMetadataEntityData {
|
||||||
UserMetadata toDto() => switch (key) {
|
UserMetadata toDto() => switch (key) {
|
||||||
UserMetadataKey.onboarding => UserMetadata(userId: userId, key: key, onboarding: Onboarding.fromMap(value)),
|
UserMetadataKey.onboarding => UserMetadata(userId: userId, key: key, onboarding: Onboarding.fromMap(value)),
|
||||||
UserMetadataKey.preferences => UserMetadata(userId: userId, key: key, preferences: Preferences.fromMap(value)),
|
UserMetadataKey.preferences => UserMetadata(userId: userId, key: key, preferences: Preferences.fromMap(value)),
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||||
final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true));
|
final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true));
|
||||||
|
|
||||||
// TODO: the user preferences need to be updated
|
|
||||||
// from the server to get live hiding/showing of memory lane
|
|
||||||
|
|
||||||
return memoryLaneProvider.maybeWhen(
|
return memoryLaneProvider.maybeWhen(
|
||||||
data: (memories) {
|
data: (memories) {
|
||||||
return memories.isEmpty || !memoriesEnabled
|
return memories.isEmpty || !memoriesEnabled
|
||||||
|
|||||||
@@ -87,9 +87,10 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final videoAsset = await ref.read(assetServiceProvider).getAsset(asset) ?? asset;
|
||||||
try {
|
try {
|
||||||
if (asset.hasLocal && asset.livePhotoVideoId == null) {
|
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
||||||
final id = asset is LocalAsset ? (asset as LocalAsset).id : (asset as RemoteAsset).localId!;
|
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
||||||
final file = await const StorageRepository().getFileForAsset(id);
|
final file = await const StorageRepository().getFileForAsset(id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw Exception('No file found for the video');
|
throw Exception('No file found for the video');
|
||||||
@@ -99,14 +100,14 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteId = (asset as RemoteAsset).id;
|
final remoteId = (videoAsset as RemoteAsset).id;
|
||||||
|
|
||||||
// Use a network URL for the video player controller
|
// Use a network URL for the video player controller
|
||||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
final isOriginalVideo = ref.read(settingsProvider).get<bool>(Setting.loadOriginalVideo);
|
final isOriginalVideo = ref.read(settingsProvider).get<bool>(Setting.loadOriginalVideo);
|
||||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||||
final String videoUrl = asset.livePhotoVideoId != null
|
final String videoUrl = videoAsset.livePhotoVideoId != null
|
||||||
? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl'
|
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
|
||||||
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
|
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
|
||||||
|
|
||||||
final source = await VideoSource.init(
|
final source = await VideoSource.init(
|
||||||
@@ -116,7 +117,7 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
return source;
|
return source;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.severe('Error creating video source for asset ${asset.name}: $error');
|
log.severe('Error creating video source for asset ${videoAsset.name}: $error');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
case 'AssetResponseDto':
|
case 'AssetResponseDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'visibility', 'timeline');
|
addDefault(value, 'visibility', 'timeline');
|
||||||
|
addDefault(value, 'createdAt', DateTime.now().toIso8601String());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'UserAdminResponseDto':
|
case 'UserAdminResponseDto':
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
cacheKey: user.profileChangedAt.toIso8601String(),
|
cacheKey: '${user.id}-${user.profileChangedAt.toIso8601String()}',
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
placeholder: (_, __) => Image.memory(kTransparentImage),
|
placeholder: (_, __) => Image.memory(kTransparentImage),
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
|
||||||
import 'package:immich_mobile/utils/provider_utils.dart';
|
import 'package:immich_mobile/utils/provider_utils.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
import 'package:immich_mobile/utils/version_compatibility.dart';
|
import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||||
@@ -277,7 +276,6 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (isBeta) {
|
if (isBeta) {
|
||||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||||
await runNewSync(ref);
|
|
||||||
context.replaceRoute(const TabShellRoute());
|
context.replaceRoute(const TabShellRoute());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
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.139.2
|
- API version: 1.139.4
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,10 @@ part of openapi.api;
|
|||||||
class AlbumsAddAssetsResponseDto {
|
class AlbumsAddAssetsResponseDto {
|
||||||
/// Returns a new [AlbumsAddAssetsResponseDto] instance.
|
/// Returns a new [AlbumsAddAssetsResponseDto] instance.
|
||||||
AlbumsAddAssetsResponseDto({
|
AlbumsAddAssetsResponseDto({
|
||||||
required this.albumSuccessCount,
|
|
||||||
required this.assetSuccessCount,
|
|
||||||
this.error,
|
this.error,
|
||||||
required this.success,
|
required this.success,
|
||||||
});
|
});
|
||||||
|
|
||||||
int albumSuccessCount;
|
|
||||||
|
|
||||||
int assetSuccessCount;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@@ -35,26 +29,20 @@ class AlbumsAddAssetsResponseDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AlbumsAddAssetsResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AlbumsAddAssetsResponseDto &&
|
||||||
other.albumSuccessCount == albumSuccessCount &&
|
|
||||||
other.assetSuccessCount == assetSuccessCount &&
|
|
||||||
other.error == error &&
|
other.error == error &&
|
||||||
other.success == success;
|
other.success == success;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(albumSuccessCount.hashCode) +
|
|
||||||
(assetSuccessCount.hashCode) +
|
|
||||||
(error == null ? 0 : error!.hashCode) +
|
(error == null ? 0 : error!.hashCode) +
|
||||||
(success.hashCode);
|
(success.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AlbumsAddAssetsResponseDto[albumSuccessCount=$albumSuccessCount, assetSuccessCount=$assetSuccessCount, error=$error, success=$success]';
|
String toString() => 'AlbumsAddAssetsResponseDto[error=$error, success=$success]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'albumSuccessCount'] = this.albumSuccessCount;
|
|
||||||
json[r'assetSuccessCount'] = this.assetSuccessCount;
|
|
||||||
if (this.error != null) {
|
if (this.error != null) {
|
||||||
json[r'error'] = this.error;
|
json[r'error'] = this.error;
|
||||||
} else {
|
} else {
|
||||||
@@ -73,8 +61,6 @@ class AlbumsAddAssetsResponseDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return AlbumsAddAssetsResponseDto(
|
return AlbumsAddAssetsResponseDto(
|
||||||
albumSuccessCount: mapValueOfType<int>(json, r'albumSuccessCount')!,
|
|
||||||
assetSuccessCount: mapValueOfType<int>(json, r'assetSuccessCount')!,
|
|
||||||
error: BulkIdErrorReason.fromJson(json[r'error']),
|
error: BulkIdErrorReason.fromJson(json[r'error']),
|
||||||
success: mapValueOfType<bool>(json, r'success')!,
|
success: mapValueOfType<bool>(json, r'success')!,
|
||||||
);
|
);
|
||||||
@@ -124,8 +110,6 @@ class AlbumsAddAssetsResponseDto {
|
|||||||
|
|
||||||
/// 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>{
|
||||||
'albumSuccessCount',
|
|
||||||
'assetSuccessCount',
|
|
||||||
'success',
|
'success',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
11
mobile/openapi/lib/model/asset_response_dto.dart
generated
11
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -14,6 +14,7 @@ class AssetResponseDto {
|
|||||||
/// Returns a new [AssetResponseDto] instance.
|
/// Returns a new [AssetResponseDto] instance.
|
||||||
AssetResponseDto({
|
AssetResponseDto({
|
||||||
required this.checksum,
|
required this.checksum,
|
||||||
|
required this.createdAt,
|
||||||
required this.deviceAssetId,
|
required this.deviceAssetId,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
this.duplicateId,
|
this.duplicateId,
|
||||||
@@ -49,6 +50,9 @@ class AssetResponseDto {
|
|||||||
/// base64 encoded sha1 hash
|
/// base64 encoded sha1 hash
|
||||||
String checksum;
|
String checksum;
|
||||||
|
|
||||||
|
/// The UTC timestamp when the asset was originally uploaded to Immich.
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
String deviceAssetId;
|
String deviceAssetId;
|
||||||
|
|
||||||
String deviceId;
|
String deviceId;
|
||||||
@@ -142,6 +146,7 @@ class AssetResponseDto {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||||
other.checksum == checksum &&
|
other.checksum == checksum &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
other.deviceAssetId == deviceAssetId &&
|
other.deviceAssetId == deviceAssetId &&
|
||||||
other.deviceId == deviceId &&
|
other.deviceId == deviceId &&
|
||||||
other.duplicateId == duplicateId &&
|
other.duplicateId == duplicateId &&
|
||||||
@@ -177,6 +182,7 @@ class AssetResponseDto {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(checksum.hashCode) +
|
(checksum.hashCode) +
|
||||||
|
(createdAt.hashCode) +
|
||||||
(deviceAssetId.hashCode) +
|
(deviceAssetId.hashCode) +
|
||||||
(deviceId.hashCode) +
|
(deviceId.hashCode) +
|
||||||
(duplicateId == null ? 0 : duplicateId!.hashCode) +
|
(duplicateId == null ? 0 : duplicateId!.hashCode) +
|
||||||
@@ -209,11 +215,12 @@ class AssetResponseDto {
|
|||||||
(visibility.hashCode);
|
(visibility.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]';
|
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'checksum'] = this.checksum;
|
json[r'checksum'] = this.checksum;
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
json[r'deviceAssetId'] = this.deviceAssetId;
|
json[r'deviceAssetId'] = this.deviceAssetId;
|
||||||
json[r'deviceId'] = this.deviceId;
|
json[r'deviceId'] = this.deviceId;
|
||||||
if (this.duplicateId != null) {
|
if (this.duplicateId != null) {
|
||||||
@@ -293,6 +300,7 @@ class AssetResponseDto {
|
|||||||
|
|
||||||
return AssetResponseDto(
|
return AssetResponseDto(
|
||||||
checksum: mapValueOfType<String>(json, r'checksum')!,
|
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
deviceAssetId: mapValueOfType<String>(json, r'deviceAssetId')!,
|
deviceAssetId: mapValueOfType<String>(json, r'deviceAssetId')!,
|
||||||
deviceId: mapValueOfType<String>(json, r'deviceId')!,
|
deviceId: mapValueOfType<String>(json, r'deviceId')!,
|
||||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||||
@@ -371,6 +379,7 @@ class AssetResponseDto {
|
|||||||
/// 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>{
|
||||||
'checksum',
|
'checksum',
|
||||||
|
'createdAt',
|
||||||
'deviceAssetId',
|
'deviceAssetId',
|
||||||
'deviceId',
|
'deviceId',
|
||||||
'duration',
|
'duration',
|
||||||
|
|||||||
@@ -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.139.2+3007
|
version: 1.139.4+3009
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
|
|||||||
@@ -9592,7 +9592,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.139.2",
|
"version": "1.139.4",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@@ -10007,12 +10007,6 @@
|
|||||||
},
|
},
|
||||||
"AlbumsAddAssetsResponseDto": {
|
"AlbumsAddAssetsResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"albumSuccessCount": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"assetSuccessCount": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"error": {
|
"error": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -10025,8 +10019,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"albumSuccessCount",
|
|
||||||
"assetSuccessCount",
|
|
||||||
"success"
|
"success"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -10728,6 +10720,12 @@
|
|||||||
"description": "base64 encoded sha1 hash",
|
"description": "base64 encoded sha1 hash",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"description": "The UTC timestamp when the asset was originally uploaded to Immich.",
|
||||||
|
"example": "2024-01-15T20:30:00.000Z",
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"deviceAssetId": {
|
"deviceAssetId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -10863,6 +10861,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"checksum",
|
"checksum",
|
||||||
|
"createdAt",
|
||||||
"deviceAssetId",
|
"deviceAssetId",
|
||||||
"deviceId",
|
"deviceId",
|
||||||
"duration",
|
"duration",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.139.2",
|
"version": "1.139.4",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.139.2
|
* 1.139.4
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
@@ -317,6 +317,8 @@ export type TagResponseDto = {
|
|||||||
export type AssetResponseDto = {
|
export type AssetResponseDto = {
|
||||||
/** base64 encoded sha1 hash */
|
/** base64 encoded sha1 hash */
|
||||||
checksum: string;
|
checksum: string;
|
||||||
|
/** The UTC timestamp when the asset was originally uploaded to Immich. */
|
||||||
|
createdAt: string;
|
||||||
deviceAssetId: string;
|
deviceAssetId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
duplicateId?: string | null;
|
duplicateId?: string | null;
|
||||||
@@ -389,8 +391,6 @@ export type AlbumsAddAssetsDto = {
|
|||||||
assetIds: string[];
|
assetIds: string[];
|
||||||
};
|
};
|
||||||
export type AlbumsAddAssetsResponseDto = {
|
export type AlbumsAddAssetsResponseDto = {
|
||||||
albumSuccessCount: number;
|
|
||||||
assetSuccessCount: number;
|
|
||||||
error?: BulkIdErrorReason;
|
error?: BulkIdErrorReason;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ export * from './fetch-errors.js';
|
|||||||
export interface InitOptions {
|
export interface InitOptions {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const init = ({ baseUrl, apiKey }: InitOptions) => {
|
export const init = ({ baseUrl, apiKey, headers }: InitOptions) => {
|
||||||
setBaseUrl(baseUrl);
|
setBaseUrl(baseUrl);
|
||||||
setApiKey(apiKey);
|
setApiKey(apiKey);
|
||||||
|
if (headers) {
|
||||||
|
setHeaders(headers);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBaseUrl = () => defaults.baseUrl;
|
export const getBaseUrl = () => defaults.baseUrl;
|
||||||
@@ -24,6 +28,26 @@ export const setApiKey = (apiKey: string) => {
|
|||||||
defaults.headers['x-api-key'] = apiKey;
|
defaults.headers['x-api-key'] = apiKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setHeader = (key: string, value: string) => {
|
||||||
|
assertNoApiKey(key);
|
||||||
|
defaults.headers = defaults.headers || {};
|
||||||
|
defaults.headers[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setHeaders = (headers: Record<string, string>) => {
|
||||||
|
defaults.headers = defaults.headers || {};
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
assertNoApiKey(key);
|
||||||
|
defaults.headers[key] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNoApiKey = (headerKey: string) => {
|
||||||
|
if (headerKey.toLowerCase() === 'x-api-key') {
|
||||||
|
throw new Error('The API key header can only be set using setApiKey().');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getAssetOriginalPath = (id: string) => `/assets/${id}/original`;
|
export const getAssetOriginalPath = (id: string) => `/assets/${id}/original`;
|
||||||
|
|
||||||
export const getAssetThumbnailPath = (id: string) => `/assets/${id}/thumbnail`;
|
export const getAssetThumbnailPath = (id: string) => `/assets/${id}/thumbnail`;
|
||||||
|
|||||||
144
pnpm-lock.yaml
generated
144
pnpm-lock.yaml
generated
@@ -11,7 +11,7 @@ overrides:
|
|||||||
|
|
||||||
packageExtensionsChecksum: sha256-DAYr0FTkvKYnvBH4muAER9UE1FVGKhqfRU4/QwA2xPQ=
|
packageExtensionsChecksum: sha256-DAYr0FTkvKYnvBH4muAER9UE1FVGKhqfRU4/QwA2xPQ=
|
||||||
|
|
||||||
pnpmfileChecksum: sha256-7GOLcTtuczNumtarIG1mbRinBOSpiOOVzgbeV3Xp4X4=
|
pnpmfileChecksum: sha256-AG/qwrPNpmy9q60PZwCpecoYVptglTHgH+N6RKQHOM0=
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -2124,6 +2124,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==}
|
resolution: {integrity: sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==}
|
||||||
engines: {node: '>=18.0'}
|
engines: {node: '>=18.0'}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.4.5':
|
||||||
|
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.19.12':
|
'@esbuild/aix-ppc64@0.19.12':
|
||||||
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
|
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2553,11 +2556,48 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.2':
|
||||||
|
resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.2':
|
||||||
|
resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.1.0':
|
||||||
|
resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm64@1.1.0':
|
'@img/sharp-libvips-linux-arm64@1.1.0':
|
||||||
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
|
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||||
|
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||||
|
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||||
|
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||||
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
|
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
@@ -2579,6 +2619,18 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.2':
|
||||||
|
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.2':
|
||||||
|
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.34.2':
|
'@img/sharp-linux-x64@0.34.2':
|
||||||
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
|
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
@@ -2597,6 +2649,29 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.2':
|
||||||
|
resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.2':
|
||||||
|
resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.2':
|
||||||
|
resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.2':
|
||||||
|
resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@immich/ui@0.24.1':
|
'@immich/ui@0.24.1':
|
||||||
resolution: {integrity: sha512-phJ9BHV0+OnKsxXD+5+Te5Amnb1N4ExYpRGSJPYFqutd5WXeN7kZGKZXd3CfcQ1e31SXRy4DsHSGdM1pY7AUgA==}
|
resolution: {integrity: sha512-phJ9BHV0+OnKsxXD+5+Te5Amnb1N4ExYpRGSJPYFqutd5WXeN7kZGKZXd3CfcQ1e31SXRy4DsHSGdM1pY7AUgA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -13888,6 +13963,11 @@ snapshots:
|
|||||||
- uglify-js
|
- uglify-js
|
||||||
- webpack-cli
|
- webpack-cli
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.4.5':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.19.12':
|
'@esbuild/aix-ppc64@0.19.12':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -14184,9 +14264,34 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.2':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.2':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.1.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.1.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm64@1.1.0':
|
'@img/sharp-libvips-linux-arm64@1.1.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -14201,6 +14306,16 @@ snapshots:
|
|||||||
'@img/sharp-libvips-linux-arm64': 1.1.0
|
'@img/sharp-libvips-linux-arm64': 1.1.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.2':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.2':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.34.2':
|
'@img/sharp-linux-x64@0.34.2':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-linux-x64': 1.1.0
|
'@img/sharp-libvips-linux-x64': 1.1.0
|
||||||
@@ -14216,6 +14331,20 @@ snapshots:
|
|||||||
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
|
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.2':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.4.5
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@immich/ui@0.24.1(@internationalized/date@3.8.2)(svelte@5.35.5)':
|
'@immich/ui@0.24.1(@internationalized/date@3.8.2)(svelte@5.35.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mdi/js': 7.4.47
|
'@mdi/js': 7.4.47
|
||||||
@@ -23758,14 +23887,27 @@ snapshots:
|
|||||||
node-gyp: 11.2.0
|
node-gyp: 11.2.0
|
||||||
semver: 7.7.2
|
semver: 7.7.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.34.2
|
||||||
|
'@img/sharp-darwin-x64': 0.34.2
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.1.0
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.1.0
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.1.0
|
||||||
'@img/sharp-libvips-linux-arm64': 1.1.0
|
'@img/sharp-libvips-linux-arm64': 1.1.0
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.1.0
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.1.0
|
||||||
'@img/sharp-libvips-linux-x64': 1.1.0
|
'@img/sharp-libvips-linux-x64': 1.1.0
|
||||||
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
|
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
|
||||||
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
|
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
|
||||||
|
'@img/sharp-linux-arm': 0.34.2
|
||||||
'@img/sharp-linux-arm64': 0.34.2
|
'@img/sharp-linux-arm64': 0.34.2
|
||||||
|
'@img/sharp-linux-s390x': 0.34.2
|
||||||
'@img/sharp-linux-x64': 0.34.2
|
'@img/sharp-linux-x64': 0.34.2
|
||||||
'@img/sharp-linuxmusl-arm64': 0.34.2
|
'@img/sharp-linuxmusl-arm64': 0.34.2
|
||||||
'@img/sharp-linuxmusl-x64': 0.34.2
|
'@img/sharp-linuxmusl-x64': 0.34.2
|
||||||
|
'@img/sharp-wasm32': 0.34.2
|
||||||
|
'@img/sharp-win32-arm64': 0.34.2
|
||||||
|
'@img/sharp-win32-ia32': 0.34.2
|
||||||
|
'@img/sharp-win32-x64': 0.34.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
|
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Licenza: AGPLv3"></a>
|
||||||
<a href="https://discord.immich.app">
|
<a href="https://discord.immich.app">
|
||||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
|
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Login With Custom URL">
|
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Accedi con url personalizzato">
|
||||||
</p>
|
</p>
|
||||||
<h3 align="center">Immich - Soluzione self-hosted ad alte prestazioni per backup di foto e video</h3>
|
<h3 align="center">Soluzione ad alte prestazioni per la gestione self-hosted di foto e video</h3>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://immich.app">
|
<a href="https://immich.app">
|
||||||
<img src="../design/immich-screenshots.png" title="Main Screenshot">
|
<img src="../design/immich-screenshots.png" title="Screenshot Principale">
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
@@ -36,64 +37,97 @@
|
|||||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Declino di responsabilità
|
## Avvertenze
|
||||||
|
|
||||||
- ⚠️ Il progetto è in una fase **molto intensa** di sviluppo.
|
- ⚠️ Il progetto è in fase di sviluppo **molto attivo**.
|
||||||
- ⚠️ Possibilità di bug e cambiamenti rilevanti.
|
- ⚠️ Possono esserci bug o cambiamenti radicali, che possono non essere retrocompatibili (breaking changes).
|
||||||
- ⚠️ **Non utilizzare l'app come unico salvataggio delle tue foto e dei tuoi video.**
|
- ⚠️ **Non usare l’app come unico modo per archiviare le tue foto e i tuoi video.**
|
||||||
- ⚠️ Utilizza sempre una tecnica [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) di backup per le foto e i video a cui tieni!
|
- ⚠️ Segui sempre la regola di backup [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) per proteggere i tuoi ricordi e le foto a cui tieni!
|
||||||
|
|
||||||
## Contenuto
|
> [!NOTE]
|
||||||
|
> La documentazione principale, comprese le guide all’installazione, si trova su https://immich.app/.
|
||||||
|
|
||||||
- [Documentazione Ufficiale](https://immich.app/docs)
|
## Link utili
|
||||||
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
|
||||||
- [Demo](#demo)
|
|
||||||
- [Funzionalità](#features)
|
|
||||||
- [Introduzione](https://immich.app/docs/overview/introduction)
|
|
||||||
- [Installazione](https://immich.app/docs/install/requirements)
|
|
||||||
- [Linee Guida per Contribuire](https://immich.app/docs/overview/support-the-project)
|
|
||||||
|
|
||||||
## Documentazione
|
- [Documentazione](https://immich.app/docs)
|
||||||
|
- [Informazioni](https://immich.app/docs/overview/introduction)
|
||||||
La documentazione ufficiale, inclusa la guida all'installazione, è disponibile qui: https://immich.app/.
|
- [Installazione](https://immich.app/docs/install/requirements)
|
||||||
|
- [Roadmap](https://immich.app/roadmap)
|
||||||
|
- [Demo](#demo)
|
||||||
|
- [Funzionalità](#funzionalità)
|
||||||
|
- [Traduzioni](https://immich.app/docs/developer/translations)
|
||||||
|
- [Contribuire](https://immich.app/docs/overview/support-the-project)
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
Prova la demo del progetto https://demo.immich.app. Sull'app mobile, imposta `https://demo.immich.app` come `Server Endpoint URL`
|
Accedi alla demo [qui](https://demo.immich.app).
|
||||||
|
Per l’app mobile puoi usare `https://demo.immich.app` come `Server Endpoint URL`.
|
||||||
|
|
||||||
```bash title="Demo Credential"
|
### Credenziali di accesso
|
||||||
Credenziali di accesso
|
|
||||||
email: demo@immich.app
|
|
||||||
password: demo
|
|
||||||
```
|
|
||||||
|
|
||||||
# Funzionalità
|
| Email | Password |
|
||||||
|
| --------------- | -------- |
|
||||||
|
| demo@immich.app | demo |
|
||||||
|
|
||||||
| Funzionalità | Mobile | Web |
|
## Funzionalità
|
||||||
| ---------------------------------------------- | ------ | --- |
|
|
||||||
| Caricamento e visualizzazione di foto e video | Sì | Sì |
|
| Funzionalità | Mobile | Web |
|
||||||
| Backup automatico quando l'app è in esecuzione | Sì | N/D |
|
| :------------------------------------------ | ------ | --- |
|
||||||
| Selezione degli album per backup | Sì | N/D |
|
| Caricare e visualizzare foto e video | Sì | Sì |
|
||||||
| Download foto e video sul dispositivo | Sì | Sì |
|
| Backup automatico all’apertura dell’app | Sì | N/D |
|
||||||
| Supporto multi utente | Sì | Sì |
|
| Evita la duplicazione dei file | Sì | Sì |
|
||||||
| Album e album condivisi | Sì | Sì |
|
| Backup selettivo di album | Sì | N/D |
|
||||||
| Barra di scorrimento con trascinamento | Sì | Sì |
|
| Scaricare foto e video sul dispositivo | Sì | Sì |
|
||||||
| Supporto formati raw | Sì | Sì |
|
| Supporto multi-utente | Sì | Sì |
|
||||||
| Visualizzazione metadata (EXIF, map) | Sì | Sì |
|
| Album e album condivisi | Sì | Sì |
|
||||||
| Ricerca per metadata, oggetti, volti e CLIP | Sì | Sì |
|
| Barra di scorrimento trascinabile | Sì | Sì |
|
||||||
| Funzioni di amministrazione degli utenti | No | Sì |
|
| Supporto ai formati RAW | Sì | Sì |
|
||||||
| Backup in background | Sì | N/D |
|
| Visualizzazione metadati (EXIF, mappa) | Sì | Sì |
|
||||||
| Scroll virtuale | Sì | Sì |
|
| Ricerca per metadati, oggetti, volti, CLIP | Sì | Sì |
|
||||||
| Supporto OAuth | Sì | Sì |
|
| Funzioni amministrative (gestione utenti) | No | Sì |
|
||||||
| API Keys | N/D | Sì |
|
| Backup in background | Sì | N/D |
|
||||||
| Backup e riproduzione di LivePhoto | iOS | Sì |
|
| Scorrimento virtuale | Sì | Sì |
|
||||||
| Archiviazione impostata dall'utente | Sì | Sì |
|
| Supporto OAuth | Sì | Sì |
|
||||||
| Condivisione pubblica | No | Sì |
|
| Chiavi API | N/D | Sì |
|
||||||
| Archivio e Preferiti | Sì | Sì |
|
| Backup e riproduzione LivePhoto/MotionPhoto | Sì | Sì |
|
||||||
| Mappa globale | Sì | Sì |
|
| Supporto immagini a 360° | No | Sì |
|
||||||
| Collaborazione con utenti | Sì | Sì |
|
| Struttura di archiviazione personalizzata | Sì | Sì |
|
||||||
| Riconoscimento facciale e categorizzazione | Sì | Sì |
|
| Condivisione pubblica | Sì | Sì |
|
||||||
| Ricordi (x anni fa) | Sì | Sì |
|
| Archivio e preferiti | Sì | Sì |
|
||||||
| Supporto offline | Sì | No |
|
| Mappa globale | Sì | Sì |
|
||||||
| Galleria sola lettura | Sì | Sì |
|
| Condivisione con partner | Sì | Sì |
|
||||||
| Foto raggruppate | Sì | Sì |
|
| Riconoscimento e raggruppamento facciale | Sì | Sì |
|
||||||
|
| Ricordi (anni fa) | Sì | Sì |
|
||||||
|
| Supporto offline | Sì | No |
|
||||||
|
| Galleria in sola lettura | Sì | Sì |
|
||||||
|
| Foto impilate | Sì | Sì |
|
||||||
|
| Tag | No | Sì |
|
||||||
|
| Vista per cartelle | Sì | Sì |
|
||||||
|
|
||||||
|
## Traduzioni
|
||||||
|
|
||||||
|
Scopri di più sulle traduzioni [qui](https://immich.app/docs/developer/translations).
|
||||||
|
|
||||||
|
<a href="https://hosted.weblate.org/engage/immich/">
|
||||||
|
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Stato traduzioni" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Attività del repository
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Cronologia delle stelle
|
||||||
|
|
||||||
|
<a href="https://star-history.com/#immich-app/immich&Date">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
|
||||||
|
<img alt="Grafico storico delle stelle" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Contributori
|
||||||
|
|
||||||
|
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||||
|
</a>
|
||||||
|
|||||||
@@ -91,9 +91,12 @@ FROM prod-builder-base AS server-prod
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./package* ./pnpm* .pnpmfile.cjs ./
|
COPY ./package* ./pnpm* .pnpmfile.cjs ./
|
||||||
COPY ./server ./server/
|
COPY ./server ./server/
|
||||||
# SHARP_IGNORE_GLOBAL_LIBVIPS because 'deploy' will always build sharp bindings from source
|
## Build server with sharp linked against system (global) libvips instead of vendored copy.
|
||||||
RUN SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \
|
## Using SHARP_IGNORE_GLOBAL_LIBVIPS previously caused arm64 (e.g. Raspberry Pi) illegal instruction
|
||||||
pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
|
## crashes due to the prebuilt vendored libvips targeting newer ARM features. Force global libvips
|
||||||
|
## during build so the already-present distro libvips (built with conservative flags) is used.
|
||||||
|
RUN SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \
|
||||||
|
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
|
||||||
|
|
||||||
# web production build
|
# web production build
|
||||||
FROM prod-builder-base AS web-prod
|
FROM prod-builder-base AS web-prod
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true,
|
"deleteOutDir": false,
|
||||||
"webpack": false,
|
"webpack": false,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.139.2",
|
"version": "1.139.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -65,10 +65,6 @@ export class AlbumsAddAssetsDto {
|
|||||||
|
|
||||||
export class AlbumsAddAssetsResponseDto {
|
export class AlbumsAddAssetsResponseDto {
|
||||||
success!: boolean;
|
success!: boolean;
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
albumSuccessCount!: number;
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
assetSuccessCount!: number;
|
|
||||||
@ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', optional: true })
|
@ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', optional: true })
|
||||||
error?: BulkIdErrorReason;
|
error?: BulkIdErrorReason;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ export class SanitizedAssetResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AssetResponseDto extends SanitizedAssetResponseDto {
|
export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'The UTC timestamp when the asset was originally uploaded to Immich.',
|
||||||
|
example: '2024-01-15T20:30:00.000Z',
|
||||||
|
})
|
||||||
|
createdAt!: Date;
|
||||||
deviceAssetId!: string;
|
deviceAssetId!: string;
|
||||||
deviceId!: string;
|
deviceId!: string;
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
@@ -190,6 +197,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
|
createdAt: entity.createdAt,
|
||||||
deviceAssetId: entity.deviceAssetId,
|
deviceAssetId: entity.deviceAssetId,
|
||||||
ownerId: entity.ownerId,
|
ownerId: entity.ownerId,
|
||||||
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ with
|
|||||||
epoch
|
epoch
|
||||||
from
|
from
|
||||||
(
|
(
|
||||||
asset."localDateTime" - asset."fileCreatedAt" at time zone 'UTC'
|
asset."localDateTime" AT TIME ZONE 'UTC' - asset."fileCreatedAt" at time zone 'UTC'
|
||||||
)
|
)
|
||||||
)::real / 3600 as "localOffsetHours",
|
)::real / 3600 as "localOffsetHours",
|
||||||
"asset"."ownerId",
|
"asset"."ownerId",
|
||||||
|
|||||||
@@ -321,6 +321,14 @@ export class AlbumRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Chunked({ chunkSize: 30_000 })
|
||||||
|
async addAssetIdsToAlbums(values: { albumsId: string; assetsId: string }[]): Promise<void> {
|
||||||
|
if (values.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.db.insertInto('album_asset').values(values).execute();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes sure all thumbnails for albums are updated by:
|
* Makes sure all thumbnails for albums are updated by:
|
||||||
* - Removing thumbnails from albums without assets
|
* - Removing thumbnails from albums without assets
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ export class AssetRepository {
|
|||||||
sql`asset.type = 'IMAGE'`.as('isImage'),
|
sql`asset.type = 'IMAGE'`.as('isImage'),
|
||||||
sql`asset."deletedAt" is not null`.as('isTrashed'),
|
sql`asset."deletedAt" is not null`.as('isTrashed'),
|
||||||
'asset.livePhotoVideoId',
|
'asset.livePhotoVideoId',
|
||||||
sql`extract(epoch from (asset."localDateTime" - asset."fileCreatedAt" at time zone 'UTC'))::real / 3600`.as(
|
sql`extract(epoch from (asset."localDateTime" AT TIME ZONE 'UTC' - asset."fileCreatedAt" at time zone 'UTC'))::real / 3600`.as(
|
||||||
'localOffsetHours',
|
'localOffsetHours',
|
||||||
),
|
),
|
||||||
'asset.ownerId',
|
'asset.ownerId',
|
||||||
|
|||||||
@@ -778,9 +778,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
describe('addAssetsToAlbums', () => {
|
describe('addAssetsToAlbums', () => {
|
||||||
it('should allow the owner to add assets', async () => {
|
it('should allow the owner to add assets', async () => {
|
||||||
mocks.access.album.checkOwnerAccess
|
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321']));
|
||||||
.mockResolvedValueOnce(new Set(['album-123']))
|
|
||||||
.mockResolvedValueOnce(new Set(['album-321']));
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||||
mocks.album.getById
|
mocks.album.getById
|
||||||
.mockResolvedValueOnce(_.cloneDeep(albumStub.empty))
|
.mockResolvedValueOnce(_.cloneDeep(albumStub.empty))
|
||||||
@@ -792,7 +790,7 @@ describe(AlbumService.name, () => {
|
|||||||
albumIds: ['album-123', 'album-321'],
|
albumIds: ['album-123', 'album-321'],
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 });
|
).resolves.toEqual({ success: true, error: undefined });
|
||||||
|
|
||||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
||||||
@@ -805,14 +803,18 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']);
|
{ albumsId: 'album-123', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-3' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-3' },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set the thumbnail if the album has one already', async () => {
|
it('should not set the thumbnail if the album has one already', async () => {
|
||||||
mocks.access.album.checkOwnerAccess
|
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321']));
|
||||||
.mockResolvedValueOnce(new Set(['album-123']))
|
|
||||||
.mockResolvedValueOnce(new Set(['album-321']));
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||||
mocks.album.getById
|
mocks.album.getById
|
||||||
.mockResolvedValueOnce(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' }))
|
.mockResolvedValueOnce(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' }))
|
||||||
@@ -824,7 +826,7 @@ describe(AlbumService.name, () => {
|
|||||||
albumIds: ['album-123', 'album-321'],
|
albumIds: ['album-123', 'album-321'],
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 });
|
).resolves.toEqual({ success: true, error: undefined });
|
||||||
|
|
||||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
||||||
@@ -837,14 +839,18 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-id',
|
albumThumbnailAssetId: 'asset-id',
|
||||||
});
|
});
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']);
|
{ albumsId: 'album-123', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-3' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-3' },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a shared user to add assets', async () => {
|
it('should allow a shared user to add assets', async () => {
|
||||||
mocks.access.album.checkSharedAlbumAccess
|
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321']));
|
||||||
.mockResolvedValueOnce(new Set(['album-123']))
|
|
||||||
.mockResolvedValueOnce(new Set(['album-321']));
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||||
mocks.album.getById
|
mocks.album.getById
|
||||||
.mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser))
|
.mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser))
|
||||||
@@ -856,7 +862,7 @@ describe(AlbumService.name, () => {
|
|||||||
albumIds: ['album-123', 'album-321'],
|
albumIds: ['album-123', 'album-321'],
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 });
|
).resolves.toEqual({ success: true, error: undefined });
|
||||||
|
|
||||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
||||||
@@ -869,8 +875,14 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']);
|
{ albumsId: 'album-123', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-3' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-3' },
|
||||||
|
]);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
recipientId: 'admin_id',
|
recipientId: 'admin_id',
|
||||||
@@ -896,18 +908,14 @@ describe(AlbumService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
albumSuccessCount: 0,
|
error: BulkIdErrorReason.NO_PERMISSION,
|
||||||
assetSuccessCount: 0,
|
|
||||||
error: BulkIdErrorReason.UNKNOWN,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow a shared link user to add assets to multiple albums', async () => {
|
it('should not allow a shared link user to add assets to multiple albums', async () => {
|
||||||
mocks.access.album.checkSharedLinkAccess
|
mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set(['album-123']));
|
||||||
.mockResolvedValueOnce(new Set(['album-123']))
|
|
||||||
.mockResolvedValueOnce(new Set());
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||||
mocks.album.getById
|
mocks.album.getById
|
||||||
.mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser))
|
.mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser))
|
||||||
@@ -919,7 +927,7 @@ describe(AlbumService.name, () => {
|
|||||||
albumIds: ['album-123', 'album-321'],
|
albumIds: ['album-123', 'album-321'],
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true, albumSuccessCount: 1, assetSuccessCount: 3 });
|
).resolves.toEqual({ success: true, error: undefined });
|
||||||
|
|
||||||
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
||||||
@@ -927,22 +935,23 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledTimes(1);
|
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
{ albumsId: 'album-123', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-3' },
|
||||||
|
]);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||||
id: 'album-123',
|
id: 'album-123',
|
||||||
recipientId: 'user-id',
|
recipientId: 'user-id',
|
||||||
});
|
});
|
||||||
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||||
authStub.adminSharedLink.sharedLink?.id,
|
authStub.adminSharedLink.sharedLink?.id,
|
||||||
new Set(['album-123']),
|
new Set(['album-123', 'album-321']),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow adding assets shared via partner sharing', async () => {
|
it('should allow adding assets shared via partner sharing', async () => {
|
||||||
mocks.access.album.checkOwnerAccess
|
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321']));
|
||||||
.mockResolvedValueOnce(new Set(['album-123']))
|
|
||||||
.mockResolvedValueOnce(new Set(['album-321']));
|
|
||||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||||
mocks.album.getById
|
mocks.album.getById
|
||||||
.mockResolvedValueOnce(_.cloneDeep(albumStub.empty))
|
.mockResolvedValueOnce(_.cloneDeep(albumStub.empty))
|
||||||
@@ -954,7 +963,7 @@ describe(AlbumService.name, () => {
|
|||||||
albumIds: ['album-123', 'album-321'],
|
albumIds: ['album-123', 'album-321'],
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 });
|
).resolves.toEqual({ success: true, error: undefined });
|
||||||
|
|
||||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', {
|
||||||
@@ -967,8 +976,14 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']);
|
{ albumsId: 'album-123', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-123', assetsId: 'asset-3' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-3' },
|
||||||
|
]);
|
||||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(
|
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(
|
||||||
authStub.admin.user.id,
|
authStub.admin.user.id,
|
||||||
new Set(['asset-1', 'asset-2', 'asset-3']),
|
new Set(['asset-1', 'asset-2', 'asset-3']),
|
||||||
@@ -976,23 +991,21 @@ describe(AlbumService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip some duplicate assets', async () => {
|
it('should skip some duplicate assets', async () => {
|
||||||
mocks.access.album.checkOwnerAccess
|
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321']));
|
||||||
.mockResolvedValueOnce(new Set(['album-123']))
|
|
||||||
.mockResolvedValueOnce(new Set(['album-321']));
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||||
mocks.album.getById
|
|
||||||
.mockResolvedValueOnce(_.cloneDeep(albumStub.empty))
|
|
||||||
.mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset));
|
|
||||||
mocks.album.getAssetIds
|
mocks.album.getAssetIds
|
||||||
.mockResolvedValueOnce(new Set(['asset-1', 'asset-2', 'asset-3']))
|
.mockResolvedValueOnce(new Set(['asset-1', 'asset-2', 'asset-3']))
|
||||||
.mockResolvedValueOnce(new Set());
|
.mockResolvedValueOnce(new Set());
|
||||||
|
mocks.album.getById
|
||||||
|
.mockResolvedValueOnce(_.cloneDeep(albumStub.empty))
|
||||||
|
.mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.addAssetsToAlbums(authStub.admin, {
|
sut.addAssetsToAlbums(authStub.admin, {
|
||||||
albumIds: ['album-123', 'album-321'],
|
albumIds: ['album-123', 'album-321'],
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true, albumSuccessCount: 1, assetSuccessCount: 3 });
|
).resolves.toEqual({ success: true, error: undefined });
|
||||||
|
|
||||||
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-321', {
|
expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-321', {
|
||||||
@@ -1000,8 +1013,11 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledTimes(1);
|
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']);
|
{ albumsId: 'album-321', assetsId: 'asset-1' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-2' },
|
||||||
|
{ albumsId: 'album-321', assetsId: 'asset-3' },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip all duplicate assets', async () => {
|
it('should skip all duplicate assets', async () => {
|
||||||
@@ -1021,8 +1037,6 @@ describe(AlbumService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
albumSuccessCount: 0,
|
|
||||||
assetSuccessCount: 0,
|
|
||||||
error: BulkIdErrorReason.DUPLICATE,
|
error: BulkIdErrorReason.DUPLICATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1046,9 +1060,7 @@ describe(AlbumService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
albumSuccessCount: 0,
|
error: BulkIdErrorReason.NO_PERMISSION,
|
||||||
assetSuccessCount: 0,
|
|
||||||
error: BulkIdErrorReason.UNKNOWN,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||||
@@ -1076,9 +1088,7 @@ describe(AlbumService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
albumSuccessCount: 0,
|
error: BulkIdErrorReason.NO_PERMISSION,
|
||||||
assetSuccessCount: 0,
|
|
||||||
error: BulkIdErrorReason.UNKNOWN,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||||
@@ -1099,9 +1109,7 @@ describe(AlbumService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
albumSuccessCount: 0,
|
error: BulkIdErrorReason.NO_PERMISSION,
|
||||||
assetSuccessCount: 0,
|
|
||||||
error: BulkIdErrorReason.UNKNOWN,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalled();
|
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -191,36 +191,57 @@ export class AlbumService extends BaseService {
|
|||||||
async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> {
|
async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> {
|
||||||
const results: AlbumsAddAssetsResponseDto = {
|
const results: AlbumsAddAssetsResponseDto = {
|
||||||
success: false,
|
success: false,
|
||||||
albumSuccessCount: 0,
|
|
||||||
assetSuccessCount: 0,
|
|
||||||
error: BulkIdErrorReason.DUPLICATE,
|
error: BulkIdErrorReason.DUPLICATE,
|
||||||
};
|
};
|
||||||
const successfulAssetIds: Set<string> = new Set();
|
|
||||||
for (const albumId of dto.albumIds) {
|
|
||||||
try {
|
|
||||||
const albumResults = await this.addAssets(auth, albumId, { ids: dto.assetIds });
|
|
||||||
|
|
||||||
let success = false;
|
const allowedAlbumIds = await this.checkAccess({
|
||||||
for (const res of albumResults) {
|
auth,
|
||||||
if (res.success) {
|
permission: Permission.AlbumAssetCreate,
|
||||||
success = true;
|
ids: dto.albumIds,
|
||||||
results.success = true;
|
});
|
||||||
results.error = undefined;
|
if (allowedAlbumIds.size === 0) {
|
||||||
successfulAssetIds.add(res.id);
|
results.error = BulkIdErrorReason.NO_PERMISSION;
|
||||||
} else if (results.error && res.error !== BulkIdErrorReason.DUPLICATE) {
|
return results;
|
||||||
results.error = BulkIdErrorReason.UNKNOWN;
|
}
|
||||||
}
|
|
||||||
}
|
const allowedAssetIds = await this.checkAccess({ auth, permission: Permission.AssetShare, ids: dto.assetIds });
|
||||||
if (success) {
|
if (allowedAssetIds.size === 0) {
|
||||||
results.albumSuccessCount++;
|
results.error = BulkIdErrorReason.NO_PERMISSION;
|
||||||
}
|
return results;
|
||||||
} catch {
|
}
|
||||||
if (results.error) {
|
|
||||||
results.error = BulkIdErrorReason.UNKNOWN;
|
const albumAssetValues: { albumsId: string; assetsId: string }[] = [];
|
||||||
}
|
const events: { id: string; recipients: string[] }[] = [];
|
||||||
|
for (const albumId of allowedAlbumIds) {
|
||||||
|
const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]);
|
||||||
|
const notPresentAssetIds = [...allowedAssetIds].filter((id) => !existingAssetIds.has(id));
|
||||||
|
if (notPresentAssetIds.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const album = await this.findOrFail(albumId, { withAssets: false });
|
||||||
|
results.error = undefined;
|
||||||
|
results.success = true;
|
||||||
|
|
||||||
|
for (const assetId of notPresentAssetIds) {
|
||||||
|
albumAssetValues.push({ albumsId: albumId, assetsId: assetId });
|
||||||
|
}
|
||||||
|
await this.albumRepository.update(albumId, {
|
||||||
|
id: albumId,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
albumThumbnailAssetId: album.albumThumbnailAssetId ?? notPresentAssetIds[0],
|
||||||
|
});
|
||||||
|
const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter(
|
||||||
|
(userId) => userId !== auth.user.id,
|
||||||
|
);
|
||||||
|
events.push({ id: albumId, recipients: allUsersExceptUs });
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.albumRepository.addAssetIdsToAlbums(albumAssetValues);
|
||||||
|
for (const event of events) {
|
||||||
|
for (const recipientId of event.recipients) {
|
||||||
|
await this.eventRepository.emit('AlbumUpdate', { id: event.id, recipientId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.assetSuccessCount = successfulAssetIds.size;
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ export class LibraryService extends BaseService {
|
|||||||
{
|
{
|
||||||
usePolling: false,
|
usePolling: false,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
stabilityThreshold: 5000,
|
||||||
|
pollInterval: 1000,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onReady: () => _resolve(),
|
onReady: () => _resolve(),
|
||||||
|
|||||||
1
server/test/fixtures/shared-link.stub.ts
vendored
1
server/test/fixtures/shared-link.stub.ts
vendored
@@ -46,6 +46,7 @@ const assetInfo: ExifResponseDto = {
|
|||||||
|
|
||||||
const assetResponse: AssetResponseDto = {
|
const assetResponse: AssetResponseDto = {
|
||||||
id: 'id_1',
|
id: 'id_1',
|
||||||
|
createdAt: today,
|
||||||
deviceAssetId: 'device_asset_id_1',
|
deviceAssetId: 'device_asset_id_1',
|
||||||
ownerId: 'user_id_1',
|
ownerId: 'user_id_1',
|
||||||
deviceId: 'device_id_1',
|
deviceId: 'device_id_1',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.139.2",
|
"version": "1.139.4",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="dark w-1/2 aspect-square bg-light rounded-3xl dark:border-[3px] dark:border-immich-dark-primary border border-transparent"
|
class="w-1/2 aspect-square bg-dark dark:bg-light rounded-3xl transition-all shadow-sm hover:shadow-xl dark:border-[3px] dark:border-immich-dark-primary border border-transparent"
|
||||||
onclick={() => themeManager.setTheme(Theme.DARK)}
|
onclick={() => themeManager.setTheme(Theme.DARK)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const oninput = () => {
|
const oninput = () => {
|
||||||
if (!value) {
|
// value can be 0
|
||||||
|
if (value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
await onEnter();
|
await onEnter();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'm': {
|
case 'Control': {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleMultiSelect();
|
handleMultiSelect();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ class ApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sleep = (ms: number) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
};
|
||||||
|
|
||||||
export const uploadRequest = async <T>(options: UploadRequestOptions): Promise<{ data: T; status: number }> => {
|
export const uploadRequest = async <T>(options: UploadRequestOptions): Promise<{ data: T; status: number }> => {
|
||||||
const { onUploadProgress: onProgress, data, url } = options;
|
const { onUploadProgress: onProgress, data, url } = options;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
|||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { downloadRequest, withError } from '$lib/utils';
|
import { downloadRequest, sleep, withError } from '$lib/utils';
|
||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
@@ -278,7 +278,12 @@ export const downloadFile = async (asset: AssetResponseDto) => {
|
|||||||
|
|
||||||
const queryParams = asQueryString(authManager.params);
|
const queryParams = asQueryString(authManager.params);
|
||||||
|
|
||||||
for (const { filename, id } of assets) {
|
for (const [i, { filename, id }] of assets.entries()) {
|
||||||
|
if (i !== 0) {
|
||||||
|
// play nice with Safari
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
|
|||||||
@@ -11,15 +11,41 @@ describe('converting time to seconds', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('parses h:m:s.S correctly', () => {
|
it('parses h:m:s.S correctly', () => {
|
||||||
expect(timeToSeconds('1:2:3.4')).toBeCloseTo(3723.4);
|
expect(timeToSeconds('1:2:3.4')).toBe(0); // Non-standard format, Luxon returns NaN
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses hhh:mm:ss.SSS correctly', () => {
|
it('parses hhh:mm:ss.SSS correctly', () => {
|
||||||
expect(timeToSeconds('100:02:03.456')).toBeCloseTo(360_123.456);
|
expect(timeToSeconds('100:02:03.456')).toBe(0); // Non-standard format, Luxon returns NaN
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores ignores double milliseconds hh:mm:ss.SSS.SSSSSS', () => {
|
it('ignores ignores double milliseconds hh:mm:ss.SSS.SSSSSS', () => {
|
||||||
expect(timeToSeconds('01:02:03.456.123456')).toBeCloseTo(3723.456);
|
expect(timeToSeconds('01:02:03.456.123456')).toBe(0); // Non-standard format, Luxon returns NaN
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test edge cases that can cause crashes
|
||||||
|
it('handles "0" string input', () => {
|
||||||
|
expect(timeToSeconds('0')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty string input', () => {
|
||||||
|
expect(timeToSeconds('')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses HH:MM format correctly', () => {
|
||||||
|
expect(timeToSeconds('01:02')).toBe(3720); // 1 hour 2 minutes = 3720 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles malformed time strings', () => {
|
||||||
|
expect(timeToSeconds('invalid')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses single hour format correctly', () => {
|
||||||
|
expect(timeToSeconds('01')).toBe(3600); // Luxon interprets "01" as 1 hour
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles time strings with invalid numbers', () => {
|
||||||
|
expect(timeToSeconds('aa:bb:cc')).toBe(0);
|
||||||
|
expect(timeToSeconds('01:bb:03')).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import { get } from 'svelte/store';
|
|||||||
* Convert time like `01:02:03.456` to seconds.
|
* Convert time like `01:02:03.456` to seconds.
|
||||||
*/
|
*/
|
||||||
export function timeToSeconds(time: string) {
|
export function timeToSeconds(time: string) {
|
||||||
const parts = time.split(':');
|
if (!time || time === '0') {
|
||||||
parts[2] = parts[2].split('.').slice(0, 2).join('.');
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const [hours, minutes, seconds] = parts.map(Number);
|
const seconds = Duration.fromISOTime(time).as('seconds');
|
||||||
|
|
||||||
return Duration.fromObject({ hours, minutes, seconds }).as('seconds');
|
return Number.isNaN(seconds) ? 0 : seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseUtcDate(date: string) {
|
export function parseUtcDate(date: string) {
|
||||||
return DateTime.fromISO(date, { zone: 'UTC' }).toUTC();
|
return DateTime.fromISO(date, { zone: 'UTC' }).toUTC();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import DuplicatesInformationModal from '$lib/modals/DuplicatesInformationModal.svelte';
|
import DuplicatesInformationModal from '$lib/modals/DuplicatesInformationModal.svelte';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { stackAssets } from '$lib/utils/asset-utils';
|
import { stackAssets } from '$lib/utils/asset-utils';
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let duplicates = $state(data.duplicates);
|
let duplicates = $state(data.duplicates);
|
||||||
|
const { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
|
||||||
const correctDuplicatesIndex = (index: number) => {
|
const correctDuplicatesIndex = (index: number) => {
|
||||||
return Math.max(0, Math.min(index, duplicates.length - 1));
|
return Math.max(0, Math.min(index, duplicates.length - 1));
|
||||||
@@ -189,9 +191,21 @@
|
|||||||
const handlePrevious = async () => {
|
const handlePrevious = async () => {
|
||||||
await correctDuplicatesIndexAndGo(Math.max(duplicatesIndex - 1, 0));
|
await correctDuplicatesIndexAndGo(Math.max(duplicatesIndex - 1, 0));
|
||||||
};
|
};
|
||||||
|
const handlePreviousShortcut = async () => {
|
||||||
|
if ($showAssetViewer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handlePrevious();
|
||||||
|
};
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
await correctDuplicatesIndexAndGo(Math.min(duplicatesIndex + 1, duplicates.length - 1));
|
await correctDuplicatesIndexAndGo(Math.min(duplicatesIndex + 1, duplicates.length - 1));
|
||||||
};
|
};
|
||||||
|
const handleNextShortcut = async () => {
|
||||||
|
if ($showAssetViewer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handleNext();
|
||||||
|
};
|
||||||
const handleLast = async () => {
|
const handleLast = async () => {
|
||||||
await correctDuplicatesIndexAndGo(duplicates.length - 1);
|
await correctDuplicatesIndexAndGo(duplicates.length - 1);
|
||||||
};
|
};
|
||||||
@@ -203,8 +217,8 @@
|
|||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
use:shortcuts={[
|
use:shortcuts={[
|
||||||
{ shortcut: { key: 'ArrowLeft' }, onShortcut: handlePrevious },
|
{ shortcut: { key: 'ArrowLeft' }, onShortcut: handlePreviousShortcut },
|
||||||
{ shortcut: { key: 'ArrowRight' }, onShortcut: handleNext },
|
{ shortcut: { key: 'ArrowRight' }, onShortcut: handleNextShortcut },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cancelLoad, getCachedOrFetch } from './fetch-event';
|
import { cancelRequest, handleRequest } from './request';
|
||||||
|
|
||||||
export const installBroadcastChannelListener = () => {
|
export const installBroadcastChannelListener = () => {
|
||||||
const broadcast = new BroadcastChannel('immich');
|
const broadcast = new BroadcastChannel('immich');
|
||||||
@@ -7,12 +7,12 @@ export const installBroadcastChannelListener = () => {
|
|||||||
if (!event.data) {
|
if (!event.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const urlstring = event.data.url;
|
const urlString = event.data.url;
|
||||||
const url = new URL(urlstring, event.origin);
|
const url = new URL(urlString, event.origin);
|
||||||
if (event.data.type === 'cancel') {
|
if (event.data.type === 'cancel') {
|
||||||
cancelLoad(url.toString());
|
cancelRequest(url);
|
||||||
} else if (event.data.type === 'preload') {
|
} else if (event.data.type === 'preload') {
|
||||||
getCachedOrFetch(url);
|
handleRequest(url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,104 +1,42 @@
|
|||||||
import { build, files, version } from '$service-worker';
|
import { version } from '$service-worker';
|
||||||
|
|
||||||
const useCache = true;
|
|
||||||
const CACHE = `cache-${version}`;
|
const CACHE = `cache-${version}`;
|
||||||
|
|
||||||
export const APP_RESOURCES = [
|
let _cache: Cache | undefined;
|
||||||
...build, // the app itself
|
const getCache = async () => {
|
||||||
...files, // everything in `static`
|
if (_cache) {
|
||||||
];
|
return _cache;
|
||||||
|
|
||||||
let cache: Cache | undefined;
|
|
||||||
export async function getCache() {
|
|
||||||
if (cache) {
|
|
||||||
return cache;
|
|
||||||
}
|
}
|
||||||
cache = await caches.open(CACHE);
|
_cache = await caches.open(CACHE);
|
||||||
return cache;
|
return _cache;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
|
export const get = async (key: string) => {
|
||||||
export const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
|
const cache = await getCache();
|
||||||
|
if (!cache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteOldCaches() {
|
return cache.match(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const put = async (key: string, response: Response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = await getCache();
|
||||||
|
if (!cache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.put(key, response.clone());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prune = async () => {
|
||||||
for (const key of await caches.keys()) {
|
for (const key of await caches.keys()) {
|
||||||
if (key !== CACHE) {
|
if (key !== CACHE) {
|
||||||
await caches.delete(key);
|
await caches.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const pendingRequests = new Map<string, AbortController>();
|
|
||||||
const canceledRequests = new Set<string>();
|
|
||||||
|
|
||||||
export async function cancelLoad(urlString: string) {
|
|
||||||
const pending = pendingRequests.get(urlString);
|
|
||||||
if (pending) {
|
|
||||||
canceledRequests.add(urlString);
|
|
||||||
pending.abort();
|
|
||||||
pendingRequests.delete(urlString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCachedOrFetch(request: URL | Request | string) {
|
|
||||||
const response = await checkCache(request);
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlString = getCacheKey(request);
|
|
||||||
const cancelToken = new AbortController();
|
|
||||||
|
|
||||||
try {
|
|
||||||
pendingRequests.set(urlString, cancelToken);
|
|
||||||
const response = await fetch(request, {
|
|
||||||
signal: cancelToken.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
checkResponse(response);
|
|
||||||
await setCached(response, urlString);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
if (canceledRequests.has(urlString)) {
|
|
||||||
canceledRequests.delete(urlString);
|
|
||||||
return new Response(undefined, {
|
|
||||||
status: 499,
|
|
||||||
statusText: 'Request canceled: Instructions unclear, accidentally interrupted myself',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
pendingRequests.delete(urlString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkCache(url: URL | Request | string) {
|
|
||||||
if (!useCache) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cache = await getCache();
|
|
||||||
return await cache.match(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setCached(response: Response, cacheKey: URL | Request | string) {
|
|
||||||
if (cache && response.status === 200) {
|
|
||||||
const cache = await getCache();
|
|
||||||
cache.put(cacheKey, response.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkResponse(response: Response) {
|
|
||||||
if (!(response instanceof Response)) {
|
|
||||||
throw new TypeError('Fetch did not return a valid Response object');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCacheKey(request: URL | Request | string) {
|
|
||||||
if (isURL(request)) {
|
|
||||||
return request.toString();
|
|
||||||
} else if (isRequest(request)) {
|
|
||||||
return request.url;
|
|
||||||
} else {
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import { version } from '$service-worker';
|
|
||||||
import { APP_RESOURCES, checkCache, getCacheKey, setCached } from './cache';
|
|
||||||
|
|
||||||
const CACHE = `cache-${version}`;
|
|
||||||
|
|
||||||
export const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
|
|
||||||
export const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
|
|
||||||
|
|
||||||
export async function deleteOldCaches() {
|
|
||||||
for (const key of await caches.keys()) {
|
|
||||||
if (key !== CACHE) {
|
|
||||||
await caches.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingLoads = new Map<string, AbortController>();
|
|
||||||
|
|
||||||
export async function cancelLoad(urlString: string) {
|
|
||||||
const pending = pendingLoads.get(urlString);
|
|
||||||
if (pending) {
|
|
||||||
pending.abort();
|
|
||||||
pendingLoads.delete(urlString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCachedOrFetch(request: URL | Request | string) {
|
|
||||||
const response = await checkCache(request);
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await fetchWithCancellation(request);
|
|
||||||
} catch {
|
|
||||||
return new Response(undefined, {
|
|
||||||
status: 499,
|
|
||||||
statusText: 'Request canceled: Instructions unclear, accidentally interrupted myself',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithCancellation(request: URL | Request | string) {
|
|
||||||
const cacheKey = getCacheKey(request);
|
|
||||||
const cancelToken = new AbortController();
|
|
||||||
|
|
||||||
try {
|
|
||||||
pendingLoads.set(cacheKey, cancelToken);
|
|
||||||
const response = await fetch(request, {
|
|
||||||
signal: cancelToken.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
checkResponse(response);
|
|
||||||
setCached(response, cacheKey);
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
pendingLoads.delete(cacheKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkResponse(response: Response) {
|
|
||||||
if (!(response instanceof Response)) {
|
|
||||||
throw new TypeError('Fetch did not return a valid Response object');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIgnoredFileType(pathname: string): boolean {
|
|
||||||
return /\.(png|ico|txt|json|ts|ttf|css|js|svelte)$/.test(pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIgnoredPath(pathname: string): boolean {
|
|
||||||
return (
|
|
||||||
/^\/(src|api)(\/.*)?$/.test(pathname) || /node_modules/.test(pathname) || /^\/@(vite|id)(\/.*)?$/.test(pathname)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAssetRequest(pathname: string): boolean {
|
|
||||||
return /^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleFetchEvent(event: FetchEvent): void {
|
|
||||||
if (event.request.method !== 'GET') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(event.request.url);
|
|
||||||
|
|
||||||
// Only handle requests to the same origin
|
|
||||||
if (url.origin !== self.location.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not cache app resources
|
|
||||||
if (APP_RESOURCES.includes(url.pathname)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache requests for thumbnails
|
|
||||||
if (isAssetRequest(url.pathname)) {
|
|
||||||
event.respondWith(getCachedOrFetch(event.request));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not cache ignored file types or paths
|
|
||||||
if (isIgnoredFileType(url.pathname) || isIgnoredPath(url.pathname)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, the only remaining requests for top level routes
|
|
||||||
// so serve the Svelte SPA fallback page
|
|
||||||
const slash = new URL('/', url.origin);
|
|
||||||
event.respondWith(getCachedOrFetch(slash));
|
|
||||||
}
|
|
||||||
@@ -3,14 +3,16 @@
|
|||||||
/// <reference lib="esnext" />
|
/// <reference lib="esnext" />
|
||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
import { installBroadcastChannelListener } from './broadcast-channel';
|
import { installBroadcastChannelListener } from './broadcast-channel';
|
||||||
import { deleteOldCaches } from './cache';
|
import { prune } from './cache';
|
||||||
import { handleFetchEvent } from './fetch-event';
|
import { handleRequest } from './request';
|
||||||
|
|
||||||
|
const ASSET_REQUEST_REGEX = /^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/;
|
||||||
|
|
||||||
const sw = globalThis as unknown as ServiceWorkerGlobalScope;
|
const sw = globalThis as unknown as ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
const handleActivate = (event: ExtendableEvent) => {
|
const handleActivate = (event: ExtendableEvent) => {
|
||||||
event.waitUntil(sw.clients.claim());
|
event.waitUntil(sw.clients.claim());
|
||||||
event.waitUntil(deleteOldCaches());
|
event.waitUntil(prune());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInstall = (event: ExtendableEvent) => {
|
const handleInstall = (event: ExtendableEvent) => {
|
||||||
@@ -18,7 +20,20 @@ const handleInstall = (event: ExtendableEvent) => {
|
|||||||
// do not preload app resources
|
// do not preload app resources
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFetch = (event: FetchEvent): void => {
|
||||||
|
if (event.request.method !== 'GET') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache requests for thumbnails
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
if (url.origin === self.location.origin && ASSET_REQUEST_REGEX.test(url.pathname)) {
|
||||||
|
event.respondWith(handleRequest(event.request));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
sw.addEventListener('install', handleInstall, { passive: true });
|
sw.addEventListener('install', handleInstall, { passive: true });
|
||||||
sw.addEventListener('activate', handleActivate, { passive: true });
|
sw.addEventListener('activate', handleActivate, { passive: true });
|
||||||
sw.addEventListener('fetch', handleFetchEvent, { passive: true });
|
sw.addEventListener('fetch', handleFetch, { passive: true });
|
||||||
installBroadcastChannelListener();
|
installBroadcastChannelListener();
|
||||||
|
|||||||
63
web/src/service-worker/request.ts
Normal file
63
web/src/service-worker/request.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { get, put } from './cache';
|
||||||
|
|
||||||
|
const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
|
||||||
|
const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
|
||||||
|
|
||||||
|
const assertResponse = (response: Response) => {
|
||||||
|
if (!(response instanceof Response)) {
|
||||||
|
throw new TypeError('Fetch did not return a valid Response object');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCacheKey = (request: URL | Request) => {
|
||||||
|
if (isURL(request)) {
|
||||||
|
return request.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRequest(request)) {
|
||||||
|
return request.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid request: ${request}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pendingRequests = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
export const handleRequest = async (request: URL | Request) => {
|
||||||
|
const cacheKey = getCacheKey(request);
|
||||||
|
|
||||||
|
const cachedResponse = await get(cacheKey);
|
||||||
|
if (cachedResponse) {
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cancelToken = new AbortController();
|
||||||
|
pendingRequests.set(cacheKey, cancelToken);
|
||||||
|
const response = await fetch(request, { signal: cancelToken.signal });
|
||||||
|
|
||||||
|
assertResponse(response);
|
||||||
|
put(cacheKey, response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return new Response(undefined, {
|
||||||
|
status: 499,
|
||||||
|
statusText: `Request canceled: Instructions unclear, accidentally interrupted myself (${error})`,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
pendingRequests.delete(cacheKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cancelRequest = (url: URL) => {
|
||||||
|
const cacheKey = getCacheKey(url);
|
||||||
|
const pending = pendingRequests.get(cacheKey);
|
||||||
|
if (!pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.abort();
|
||||||
|
pendingRequests.delete(cacheKey);
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { Sync } from 'factory.ts';
|
|||||||
|
|
||||||
export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
||||||
id: Sync.each(() => faker.string.uuid()),
|
id: Sync.each(() => faker.string.uuid()),
|
||||||
|
createdAt: Sync.each(() => faker.date.past().toISOString()),
|
||||||
deviceAssetId: Sync.each(() => faker.string.uuid()),
|
deviceAssetId: Sync.each(() => faker.string.uuid()),
|
||||||
ownerId: Sync.each(() => faker.string.uuid()),
|
ownerId: Sync.each(() => faker.string.uuid()),
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user