mirror of
https://github.com/immich-app/immich.git
synced 2025-07-08 02:36:40 -04:00
Merge branch 'main' of github.com:immich-app/immich into mobile/collections
This commit is contained in:
commit
c2e4d91b69
@ -1,16 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
|
mdiBug,
|
||||||
mdiCalendarToday,
|
mdiCalendarToday,
|
||||||
mdiCrosshairsOff,
|
mdiCrosshairsOff,
|
||||||
|
mdiDatabase,
|
||||||
mdiLeadPencil,
|
mdiLeadPencil,
|
||||||
mdiLockOff,
|
mdiLockOff,
|
||||||
mdiLockOutline,
|
mdiLockOutline,
|
||||||
|
mdiSecurity,
|
||||||
mdiSpeedometerSlow,
|
mdiSpeedometerSlow,
|
||||||
|
mdiTrashCan,
|
||||||
mdiWeb,
|
mdiWeb,
|
||||||
mdiWrap,
|
mdiWrap,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Item as TimelineItem, Timeline } from '../components/timeline';
|
import { Timeline, Item as TimelineItem } from '../components/timeline';
|
||||||
|
|
||||||
const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language);
|
const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language);
|
||||||
|
|
||||||
@ -96,6 +100,51 @@ const items: Item[] = [
|
|||||||
link: { url: 'https://github.com/immich-app/immich/pull/6787', text: '#6787' },
|
link: { url: 'https://github.com/immich-app/immich/pull/6787', text: '#6787' },
|
||||||
date: new Date(2024, 0, 31),
|
date: new Date(2024, 0, 31),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: mdiBug,
|
||||||
|
iconColor: 'green',
|
||||||
|
title: 'ESM imports are cursed',
|
||||||
|
description:
|
||||||
|
'Prior to Node.js v20.8 using --experimental-vm-modules in a CommonJS project that imported an ES module that imported a CommonJS modules would create a segfault and crash Node.js',
|
||||||
|
link: {
|
||||||
|
url: 'https://github.com/immich-app/immich/pull/6719',
|
||||||
|
text: '#6179',
|
||||||
|
},
|
||||||
|
date: new Date(2024, 0, 9),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiDatabase,
|
||||||
|
iconColor: 'gray',
|
||||||
|
title: 'PostgreSQL parameters are cursed',
|
||||||
|
description: `PostgresSQL has a limit of ${Number(65535).toLocaleString()} parameters, so bulk inserts can fail with large datasets.`,
|
||||||
|
link: {
|
||||||
|
url: 'https://github.com/immich-app/immich/pull/6034',
|
||||||
|
text: '#6034',
|
||||||
|
},
|
||||||
|
date: new Date(2023, 11, 28),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiSecurity,
|
||||||
|
iconColor: 'gold',
|
||||||
|
title: 'Secure contexts are cursed',
|
||||||
|
description: `Some web features like the clipboard API only work in "secure contexts" (ie. https or localhost)`,
|
||||||
|
link: {
|
||||||
|
url: 'https://github.com/immich-app/immich/issues/2981',
|
||||||
|
text: '#2981',
|
||||||
|
},
|
||||||
|
date: new Date(2023, 5, 26),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiTrashCan,
|
||||||
|
iconColor: 'gray',
|
||||||
|
title: 'TypeORM deletes are cursed',
|
||||||
|
description: `The remove implementation in TypeORM mutates the input, deleting the id property from the original object.`,
|
||||||
|
link: {
|
||||||
|
url: 'https://github.com/typeorm/typeorm/issues/7024#issuecomment-948519328',
|
||||||
|
text: 'typeorm#6034',
|
||||||
|
},
|
||||||
|
date: new Date(2023, 1, 23),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function CursedKnowledgePage(): JSX.Element {
|
export default function CursedKnowledgePage(): JSX.Element {
|
||||||
|
@ -545,6 +545,38 @@ describe('/asset', () => {
|
|||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not allow linking two photos', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ livePhotoVideoId: user1Assets[1].id });
|
||||||
|
|
||||||
|
expect(body).toEqual(errorDto.badRequest('Live photo video must be a video'));
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow linking a video owned by another user', async () => {
|
||||||
|
const asset = await utils.createAsset(user2.accessToken, { assetData: { filename: 'example.mp4' } });
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ livePhotoVideoId: asset.id });
|
||||||
|
|
||||||
|
expect(body).toEqual(errorDto.badRequest('Live photo video does not belong to the user'));
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should link a motion photo', async () => {
|
||||||
|
const asset = await utils.createAsset(user1.accessToken, { assetData: { filename: 'example.mp4' } });
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ livePhotoVideoId: asset.id });
|
||||||
|
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: asset.id });
|
||||||
|
});
|
||||||
|
|
||||||
it('should update date time original when sidecar file contains DateTimeOriginal', async () => {
|
it('should update date time original when sidecar file contains DateTimeOriginal', async () => {
|
||||||
const sidecarData = `<?xpacket begin='?' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
const sidecarData = `<?xpacket begin='?' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
||||||
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.40'>
|
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.40'>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
ARG DEVICE=cpu
|
ARG DEVICE=cpu
|
||||||
|
|
||||||
FROM python:3.11-bookworm@sha256:20c1819af5af3acba0b2b66074a2615e398ceee6842adf03cd7ad5f8d0ee3daf AS builder-cpu
|
FROM python:3.11-bookworm@sha256:3cd9b520be95c671135ea1318f32be6912876024ee16d0f472669d3878801651 AS builder-cpu
|
||||||
|
|
||||||
FROM builder-cpu AS builder-openvino
|
FROM builder-cpu AS builder-openvino
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
|
|||||||
COPY poetry.lock pyproject.toml ./
|
COPY poetry.lock pyproject.toml ./
|
||||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm@sha256:ed4e985674f478c90ce879e9aa224fbb772c84e39b4aed5155b9e2280f131039 AS prod-cpu
|
FROM python:3.11-slim-bookworm@sha256:50ec89bdac0a845ec1751f91cb6187a3d8adb2b919d6e82d17acf48d1a9743fc AS prod-cpu
|
||||||
|
|
||||||
FROM prod-cpu AS prod-openvino
|
FROM prod-cpu AS prod-openvino
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:29174348bd09352e5f1b1f6756cf1d00021487b8340fae040e91e4f98e954ce5 AS builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:b10f75974a30a6889b03519ac48d3e1510fd13d0689468c2c443033a15d84f1b AS builder
|
||||||
|
|
||||||
ENV TRANSFORMERS_CACHE=/cache \
|
ENV TRANSFORMERS_CACHE=/cache \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
52
machine-learning/poetry.lock
generated
52
machine-learning/poetry.lock
generated
@ -1963,36 +1963,36 @@ reference = ["Pillow", "google-re2"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime"
|
name = "onnxruntime"
|
||||||
version = "1.19.0"
|
version = "1.19.2"
|
||||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "onnxruntime-1.19.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6ce22a98dfec7b646ae305f52d0ce14a189a758b02ea501860ca719f4b0ae04b"},
|
{file = "onnxruntime-1.19.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:84fa57369c06cadd3c2a538ae2a26d76d583e7c34bdecd5769d71ca5c0fc750e"},
|
||||||
{file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19019c72873f26927aa322c54cf2bf7312b23451b27451f39b88f57016c94f8b"},
|
{file = "onnxruntime-1.19.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdc471a66df0c1cdef774accef69e9f2ca168c851ab5e4f2f3341512c7ef4666"},
|
||||||
{file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8eaa16df99171dc636e30108d15597aed8c4c2dd9dbfdd07cc464d57d73fb275"},
|
{file = "onnxruntime-1.19.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e3a4ce906105d99ebbe817f536d50a91ed8a4d1592553f49b3c23c4be2560ae6"},
|
||||||
{file = "onnxruntime-1.19.0-cp310-cp310-win32.whl", hash = "sha256:0eb0f8dbe596fd0f4737fe511fdbb17603853a7d204c5b2ca38d3c7808fc556b"},
|
{file = "onnxruntime-1.19.2-cp310-cp310-win32.whl", hash = "sha256:4b3d723cc154c8ddeb9f6d0a8c0d6243774c6b5930847cc83170bfe4678fafb3"},
|
||||||
{file = "onnxruntime-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:616092d54ba8023b7bc0a5f6d900a07a37cc1cfcc631873c15f8c1d6e9e184d4"},
|
{file = "onnxruntime-1.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:17ed7382d2c58d4b7354fb2b301ff30b9bf308a1c7eac9546449cd122d21cae5"},
|
||||||
{file = "onnxruntime-1.19.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a2b53b3c287cd933e5eb597273926e899082d8c84ab96e1b34035764a1627e17"},
|
{file = "onnxruntime-1.19.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d863e8acdc7232d705d49e41087e10b274c42f09e259016a46f32c34e06dc4fd"},
|
||||||
{file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e94984663963e74fbb468bde9ec6f19dcf890b594b35e249c4dc8789d08993c5"},
|
{file = "onnxruntime-1.19.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dfe4f660a71b31caa81fc298a25f9612815215a47b286236e61d540350d7b6"},
|
||||||
{file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f379d1f050cfb55ce015d53727b78ee362febc065c38eed81512b22b757da73"},
|
{file = "onnxruntime-1.19.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a36511dc07c5c964b916697e42e366fa43c48cdb3d3503578d78cef30417cb84"},
|
||||||
{file = "onnxruntime-1.19.0-cp311-cp311-win32.whl", hash = "sha256:4ccb48faea02503275ae7e79e351434fc43c294c4cb5c4d8bcb7479061396614"},
|
{file = "onnxruntime-1.19.2-cp311-cp311-win32.whl", hash = "sha256:50cbb8dc69d6befad4746a69760e5b00cc3ff0a59c6c3fb27f8afa20e2cab7e7"},
|
||||||
{file = "onnxruntime-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:9cdc8d311289a84e77722de68bd22b8adfb94eea26f4be6f9e017350faac8b18"},
|
{file = "onnxruntime-1.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:1c3e5d415b78337fa0b1b75291e9ea9fb2a4c1f148eb5811e7212fed02cfffa8"},
|
||||||
{file = "onnxruntime-1.19.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1b59eaec1be9a8613c5fdeaafe67f73a062edce3ac03bbbdc9e2d98b58a30617"},
|
{file = "onnxruntime-1.19.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:68e7051bef9cfefcbb858d2d2646536829894d72a4130c24019219442b1dd2ed"},
|
||||||
{file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be4144d014a4b25184e63ce7a463a2e7796e2f3df931fccc6a6aefa6f1365dc5"},
|
{file = "onnxruntime-1.19.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2d366fbcc205ce68a8a3bde2185fd15c604d9645888703785b61ef174265168"},
|
||||||
{file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d7e7d4ca7021ce7f29a66dbc6071addf2de5839135339bd855c6d9c2bba371"},
|
{file = "onnxruntime-1.19.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:477b93df4db467e9cbf34051662a4b27c18e131fa1836e05974eae0d6e4cf29b"},
|
||||||
{file = "onnxruntime-1.19.0-cp312-cp312-win32.whl", hash = "sha256:87f2c58b577a1fb31dc5d92b647ecc588fd5f1ea0c3ad4526f5f80a113357c8d"},
|
{file = "onnxruntime-1.19.2-cp312-cp312-win32.whl", hash = "sha256:9a174073dc5608fad05f7cf7f320b52e8035e73d80b0a23c80f840e5a97c0147"},
|
||||||
{file = "onnxruntime-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a1f50d49676d7b69566536ff039d9e4e95fc482a55673719f46528218ecbb94"},
|
{file = "onnxruntime-1.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:190103273ea4507638ffc31d66a980594b237874b65379e273125150eb044857"},
|
||||||
{file = "onnxruntime-1.19.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:71423c8c4b2d7a58956271534302ec72721c62a41efd0c4896343249b8399ab0"},
|
{file = "onnxruntime-1.19.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:636bc1d4cc051d40bc52e1f9da87fbb9c57d9d47164695dfb1c41646ea51ea66"},
|
||||||
{file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d63630d45e9498f96e75bbeb7fd4a56acb10155de0de4d0e18d1b6cbb0b358a"},
|
{file = "onnxruntime-1.19.2-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bd8b875757ea941cbcfe01582970cc299893d1b65bd56731e326a8333f638a3"},
|
||||||
{file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3bfd15db1e8794d379a86c1a9116889f47f2cca40cc82208fc4f7e8c38e8522"},
|
{file = "onnxruntime-1.19.2-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2046fc9560f97947bbc1acbe4c6d48585ef0f12742744307d3364b131ac5778"},
|
||||||
{file = "onnxruntime-1.19.0-cp38-cp38-win32.whl", hash = "sha256:3b098003b6b4cb37cc84942e5f1fe27f945dd857cbd2829c824c26b0ba4a247e"},
|
{file = "onnxruntime-1.19.2-cp38-cp38-win32.whl", hash = "sha256:31c12840b1cde4ac1f7d27d540c44e13e34f2345cf3642762d2a3333621abb6a"},
|
||||||
{file = "onnxruntime-1.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:cea067a6541d6787d903ee6843401c5b1332a266585160d9700f9f0939443886"},
|
{file = "onnxruntime-1.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:016229660adea180e9a32ce218b95f8f84860a200f0f13b50070d7d90e92956c"},
|
||||||
{file = "onnxruntime-1.19.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c4fcff12dc5ca963c5f76b9822bb404578fa4a98c281e8c666b429192799a099"},
|
{file = "onnxruntime-1.19.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:006c8d326835c017a9e9f74c9c77ebb570a71174a1e89fe078b29a557d9c3848"},
|
||||||
{file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6dcad8a4db908fbe70b98c79cea1c8b6ac3316adf4ce93453136e33a524ac59"},
|
{file = "onnxruntime-1.19.2-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df2a94179a42d530b936f154615b54748239c2908ee44f0d722cb4df10670f68"},
|
||||||
{file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bc449907c6e8d99eee5ae5cc9c8fdef273d801dcd195393d3f9ab8ad3f49522"},
|
{file = "onnxruntime-1.19.2-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fae4b4de45894b9ce7ae418c5484cbf0341db6813effec01bb2216091c52f7fb"},
|
||||||
{file = "onnxruntime-1.19.0-cp39-cp39-win32.whl", hash = "sha256:947febd48405afcf526e45ccff97ff23b15e530434705f734870d22ae7fcf236"},
|
{file = "onnxruntime-1.19.2-cp39-cp39-win32.whl", hash = "sha256:dc5430f473e8706fff837ae01323be9dcfddd3ea471c900a91fa7c9b807ec5d3"},
|
||||||
{file = "onnxruntime-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:f60be47eff5ee77fd28a466b0fd41d7debc42a32179d1ddb21e05d6067d7b48b"},
|
{file = "onnxruntime-1.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:38475e29a95c5f6c62c2c603d69fc7d4c6ccbf4df602bd567b86ae1138881c49"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -414,7 +414,7 @@
|
|||||||
"search_filter_people_title": "Select people",
|
"search_filter_people_title": "Select people",
|
||||||
"search_page_categories": "Categorías",
|
"search_page_categories": "Categorías",
|
||||||
"search_page_favorites": "Favoritos",
|
"search_page_favorites": "Favoritos",
|
||||||
"search_page_motion_photos": "Fotos en .ovimiento",
|
"search_page_motion_photos": "Fotos en movimiento",
|
||||||
"search_page_no_objects": "No hay información de objetos disponible",
|
"search_page_no_objects": "No hay información de objetos disponible",
|
||||||
"search_page_no_places": "No hay información de lugares disponible",
|
"search_page_no_places": "No hay información de lugares disponible",
|
||||||
"search_page_people": "Personas",
|
"search_page_people": "Personas",
|
||||||
|
@ -16,6 +16,7 @@ class AssetBulkUploadCheckResult {
|
|||||||
required this.action,
|
required this.action,
|
||||||
this.assetId,
|
this.assetId,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
this.isTrashed,
|
||||||
this.reason,
|
this.reason,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,6 +32,14 @@ class AssetBulkUploadCheckResult {
|
|||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
bool? isTrashed;
|
||||||
|
|
||||||
AssetBulkUploadCheckResultReasonEnum? reason;
|
AssetBulkUploadCheckResultReasonEnum? reason;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -38,6 +47,7 @@ class AssetBulkUploadCheckResult {
|
|||||||
other.action == action &&
|
other.action == action &&
|
||||||
other.assetId == assetId &&
|
other.assetId == assetId &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
|
other.isTrashed == isTrashed &&
|
||||||
other.reason == reason;
|
other.reason == reason;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -46,10 +56,11 @@ class AssetBulkUploadCheckResult {
|
|||||||
(action.hashCode) +
|
(action.hashCode) +
|
||||||
(assetId == null ? 0 : assetId!.hashCode) +
|
(assetId == null ? 0 : assetId!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
|
(isTrashed == null ? 0 : isTrashed!.hashCode) +
|
||||||
(reason == null ? 0 : reason!.hashCode);
|
(reason == null ? 0 : reason!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetBulkUploadCheckResult[action=$action, assetId=$assetId, id=$id, reason=$reason]';
|
String toString() => 'AssetBulkUploadCheckResult[action=$action, assetId=$assetId, id=$id, isTrashed=$isTrashed, reason=$reason]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -60,6 +71,11 @@ class AssetBulkUploadCheckResult {
|
|||||||
// json[r'assetId'] = null;
|
// json[r'assetId'] = null;
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
|
if (this.isTrashed != null) {
|
||||||
|
json[r'isTrashed'] = this.isTrashed;
|
||||||
|
} else {
|
||||||
|
// json[r'isTrashed'] = null;
|
||||||
|
}
|
||||||
if (this.reason != null) {
|
if (this.reason != null) {
|
||||||
json[r'reason'] = this.reason;
|
json[r'reason'] = this.reason;
|
||||||
} else {
|
} else {
|
||||||
@ -79,6 +95,7 @@ class AssetBulkUploadCheckResult {
|
|||||||
action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!,
|
action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!,
|
||||||
assetId: mapValueOfType<String>(json, r'assetId'),
|
assetId: mapValueOfType<String>(json, r'assetId'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isTrashed: mapValueOfType<bool>(json, r'isTrashed'),
|
||||||
reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']),
|
reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
19
mobile/openapi/lib/model/update_asset_dto.dart
generated
19
mobile/openapi/lib/model/update_asset_dto.dart
generated
@ -18,6 +18,7 @@ class UpdateAssetDto {
|
|||||||
this.isArchived,
|
this.isArchived,
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
|
this.livePhotoVideoId,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
this.rating,
|
this.rating,
|
||||||
});
|
});
|
||||||
@ -62,6 +63,14 @@ class UpdateAssetDto {
|
|||||||
///
|
///
|
||||||
num? latitude;
|
num? latitude;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? livePhotoVideoId;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// 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
|
||||||
@ -87,6 +96,7 @@ class UpdateAssetDto {
|
|||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.latitude == latitude &&
|
other.latitude == latitude &&
|
||||||
|
other.livePhotoVideoId == livePhotoVideoId &&
|
||||||
other.longitude == longitude &&
|
other.longitude == longitude &&
|
||||||
other.rating == rating;
|
other.rating == rating;
|
||||||
|
|
||||||
@ -98,11 +108,12 @@ class UpdateAssetDto {
|
|||||||
(isArchived == null ? 0 : isArchived!.hashCode) +
|
(isArchived == null ? 0 : isArchived!.hashCode) +
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
(latitude == null ? 0 : latitude!.hashCode) +
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
|
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||||
(longitude == null ? 0 : longitude!.hashCode) +
|
(longitude == null ? 0 : longitude!.hashCode) +
|
||||||
(rating == null ? 0 : rating!.hashCode);
|
(rating == null ? 0 : rating!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
|
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -131,6 +142,11 @@ class UpdateAssetDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'latitude'] = null;
|
// json[r'latitude'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.livePhotoVideoId != null) {
|
||||||
|
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
|
||||||
|
} else {
|
||||||
|
// json[r'livePhotoVideoId'] = null;
|
||||||
|
}
|
||||||
if (this.longitude != null) {
|
if (this.longitude != null) {
|
||||||
json[r'longitude'] = this.longitude;
|
json[r'longitude'] = this.longitude;
|
||||||
} else {
|
} else {
|
||||||
@ -157,6 +173,7 @@ class UpdateAssetDto {
|
|||||||
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
isArchived: mapValueOfType<bool>(json, r'isArchived'),
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
latitude: num.parse('${json[r'latitude']}'),
|
latitude: num.parse('${json[r'latitude']}'),
|
||||||
|
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||||
longitude: num.parse('${json[r'longitude']}'),
|
longitude: num.parse('${json[r'longitude']}'),
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: num.parse('${json[r'rating']}'),
|
||||||
);
|
);
|
||||||
|
@ -7928,6 +7928,9 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"isTrashed": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"reason": {
|
"reason": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"duplicate",
|
"duplicate",
|
||||||
@ -12238,6 +12241,10 @@
|
|||||||
"latitude": {
|
"latitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"livePhotoVideoId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"longitude": {
|
"longitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
@ -395,6 +395,7 @@ export type AssetBulkUploadCheckResult = {
|
|||||||
action: Action;
|
action: Action;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
isTrashed?: boolean;
|
||||||
reason?: Reason;
|
reason?: Reason;
|
||||||
};
|
};
|
||||||
export type AssetBulkUploadCheckResponseDto = {
|
export type AssetBulkUploadCheckResponseDto = {
|
||||||
@ -426,6 +427,7 @@ export type UpdateAssetDto = {
|
|||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
|
livePhotoVideoId?: string;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
};
|
};
|
||||||
|
@ -301,7 +301,7 @@ export class StorageCore {
|
|||||||
return this.assetRepository.update({ id, sidecarPath: newPath });
|
return this.assetRepository.update({ id, sidecarPath: newPath });
|
||||||
}
|
}
|
||||||
case PersonPathType.FACE: {
|
case PersonPathType.FACE: {
|
||||||
return this.personRepository.update([{ id, thumbnailPath: newPath }]);
|
return this.personRepository.update({ id, thumbnailPath: newPath });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ export class AssetBulkUploadCheckResult {
|
|||||||
action!: AssetUploadAction;
|
action!: AssetUploadAction;
|
||||||
reason?: AssetRejectReason;
|
reason?: AssetRejectReason;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
|
isTrashed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssetBulkUploadCheckResponseDto {
|
export class AssetBulkUploadCheckResponseDto {
|
||||||
|
@ -68,6 +68,9 @@ export class UpdateAssetDto extends UpdateAssetBase {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@IsString()
|
@IsString()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ValidateUUID({ optional: true })
|
||||||
|
livePhotoVideoId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RandomAssetsDto {
|
export class RandomAssetsDto {
|
||||||
|
@ -17,9 +17,10 @@ type EmitEventMap = {
|
|||||||
'album.update': [{ id: string; updatedBy: string }];
|
'album.update': [{ id: string; updatedBy: string }];
|
||||||
'album.invite': [{ id: string; userId: string }];
|
'album.invite': [{ id: string; userId: string }];
|
||||||
|
|
||||||
// tag events
|
// asset events
|
||||||
'asset.tag': [{ assetId: string }];
|
'asset.tag': [{ assetId: string }];
|
||||||
'asset.untag': [{ assetId: string }];
|
'asset.untag': [{ assetId: string }];
|
||||||
|
'asset.hide': [{ assetId: string; userId: string }];
|
||||||
|
|
||||||
// session events
|
// session events
|
||||||
'session.delete': [{ sessionId: string }];
|
'session.delete': [{ sessionId: string }];
|
||||||
|
@ -54,7 +54,8 @@ export interface IPersonRepository {
|
|||||||
|
|
||||||
getAssets(personId: string): Promise<AssetEntity[]>;
|
getAssets(personId: string): Promise<AssetEntity[]>;
|
||||||
|
|
||||||
create(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]>;
|
create(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||||
|
createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
|
||||||
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
||||||
delete(entities: PersonEntity[]): Promise<void>;
|
delete(entities: PersonEntity[]): Promise<void>;
|
||||||
deleteAll(): Promise<void>;
|
deleteAll(): Promise<void>;
|
||||||
@ -74,6 +75,7 @@ export interface IPersonRepository {
|
|||||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||||
update(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]>;
|
update(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||||
|
updateAll(people: Partial<PersonEntity>[]): Promise<void>;
|
||||||
getLatestFaceDate(): Promise<string | undefined>;
|
getLatestFaceDate(): Promise<string | undefined>;
|
||||||
}
|
}
|
||||||
|
@ -493,6 +493,7 @@ LIMIT
|
|||||||
-- AssetRepository.getByChecksums
|
-- AssetRepository.getByChecksums
|
||||||
SELECT
|
SELECT
|
||||||
"AssetEntity"."id" AS "AssetEntity_id",
|
"AssetEntity"."id" AS "AssetEntity_id",
|
||||||
|
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
|
||||||
"AssetEntity"."checksum" AS "AssetEntity_checksum"
|
"AssetEntity"."checksum" AS "AssetEntity_checksum"
|
||||||
FROM
|
FROM
|
||||||
"assets" "AssetEntity"
|
"assets" "AssetEntity"
|
||||||
|
@ -8,7 +8,7 @@ FROM
|
|||||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
AND ("asset"."deletedAt" IS NULL)
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
WHERE
|
WHERE
|
||||||
"asset"."ownerId" = $1
|
"asset"."ownerId" IN ($1)
|
||||||
|
|
||||||
-- MetadataRepository.getStates
|
-- MetadataRepository.getStates
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
@ -18,7 +18,7 @@ FROM
|
|||||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
AND ("asset"."deletedAt" IS NULL)
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
WHERE
|
WHERE
|
||||||
"asset"."ownerId" = $1
|
"asset"."ownerId" IN ($1)
|
||||||
AND "exif"."country" = $2
|
AND "exif"."country" = $2
|
||||||
|
|
||||||
-- MetadataRepository.getCities
|
-- MetadataRepository.getCities
|
||||||
@ -29,7 +29,7 @@ FROM
|
|||||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
AND ("asset"."deletedAt" IS NULL)
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
WHERE
|
WHERE
|
||||||
"asset"."ownerId" = $1
|
"asset"."ownerId" IN ($1)
|
||||||
AND "exif"."country" = $2
|
AND "exif"."country" = $2
|
||||||
AND "exif"."state" = $3
|
AND "exif"."state" = $3
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ FROM
|
|||||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
AND ("asset"."deletedAt" IS NULL)
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
WHERE
|
WHERE
|
||||||
"asset"."ownerId" = $1
|
"asset"."ownerId" IN ($1)
|
||||||
AND "exif"."model" = $2
|
AND "exif"."model" = $2
|
||||||
|
|
||||||
-- MetadataRepository.getCameraModels
|
-- MetadataRepository.getCameraModels
|
||||||
@ -52,5 +52,5 @@ FROM
|
|||||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
AND ("asset"."deletedAt" IS NULL)
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
WHERE
|
WHERE
|
||||||
"asset"."ownerId" = $1
|
"asset"."ownerId" IN ($1)
|
||||||
AND "exif"."make" = $2
|
AND "exif"."make" = $2
|
||||||
|
@ -338,6 +338,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
checksum: true,
|
checksum: true,
|
||||||
|
deletedAt: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
ownerId,
|
ownerId,
|
||||||
|
@ -55,7 +55,7 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
async getCountries(userIds: string[]): Promise<string[]> {
|
async getCountries(userIds: string[]): Promise<string[]> {
|
||||||
const results = await this.exifRepository
|
const results = await this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
@ -68,7 +68,7 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
return results.map(({ country }) => country).filter((item) => item !== '');
|
return results.map(({ country }) => country).filter((item) => item !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
|
||||||
async getStates(userIds: string[], country: string | undefined): Promise<string[]> {
|
async getStates(userIds: string[], country: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
@ -86,7 +86,7 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
return result.map(({ state }) => state).filter((item) => item !== '');
|
return result.map(({ state }) => state).filter((item) => item !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] })
|
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] })
|
||||||
async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise<string[]> {
|
async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
@ -108,7 +108,7 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
return results.map(({ city }) => city).filter((item) => item !== '');
|
return results.map(({ city }) => city).filter((item) => item !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
|
||||||
async getCameraMakes(userIds: string[], model: string | undefined): Promise<string[]> {
|
async getCameraMakes(userIds: string[], model: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
@ -125,7 +125,7 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
return results.map(({ make }) => make).filter((item) => item !== '');
|
return results.map(({ make }) => make).filter((item) => item !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
|
||||||
async getCameraModels(userIds: string[], make: string | undefined): Promise<string[]> {
|
async getCameraModels(userIds: string[], make: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
|
@ -280,8 +280,13 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
create(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]> {
|
create(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||||
return this.personRepository.save(entities);
|
return this.save(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAll(people: Partial<PersonEntity>[]): Promise<string[]> {
|
||||||
|
const results = await this.personRepository.save(people);
|
||||||
|
return results.map((person) => person.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFaces(entities: AssetFaceEntity[]): Promise<string[]> {
|
async createFaces(entities: AssetFaceEntity[]): Promise<string[]> {
|
||||||
@ -297,8 +302,12 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]> {
|
async update(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||||
return await this.personRepository.save(entities);
|
return this.save(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAll(people: Partial<PersonEntity>[]): Promise<void> {
|
||||||
|
await this.personRepository.save(people);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
|
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
|
||||||
@ -320,4 +329,9 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
.getRawOne();
|
.getRawOne();
|
||||||
return result?.latestDate;
|
return result?.latestDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async save(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||||
|
const { id } = await this.personRepository.save(person);
|
||||||
|
return this.personRepository.findOneByOrFail({ id });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -589,8 +589,20 @@ describe(AssetMediaService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
results: [
|
results: [
|
||||||
{ id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
{
|
||||||
{ id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
id: '1',
|
||||||
|
assetId: 'asset-1',
|
||||||
|
action: AssetUploadAction.REJECT,
|
||||||
|
reason: AssetRejectReason.DUPLICATE,
|
||||||
|
isTrashed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
assetId: 'asset-2',
|
||||||
|
action: AssetUploadAction.REJECT,
|
||||||
|
reason: AssetRejectReason.DUPLICATE,
|
||||||
|
isTrashed: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { fromChecksum } from 'src/utils/request';
|
import { fromChecksum } from 'src/utils/request';
|
||||||
@ -158,20 +158,10 @@ export class AssetMediaService {
|
|||||||
this.requireQuota(auth, file.size);
|
this.requireQuota(auth, file.size);
|
||||||
|
|
||||||
if (dto.livePhotoVideoId) {
|
if (dto.livePhotoVideoId) {
|
||||||
const motionAsset = await this.assetRepository.getById(dto.livePhotoVideoId);
|
await onBeforeLink(
|
||||||
if (!motionAsset) {
|
{ asset: this.assetRepository, event: this.eventRepository },
|
||||||
throw new BadRequestException('Live photo video not found');
|
{ userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
|
||||||
}
|
);
|
||||||
if (motionAsset.type !== AssetType.VIDEO) {
|
|
||||||
throw new BadRequestException('Live photo video must be a video');
|
|
||||||
}
|
|
||||||
if (motionAsset.ownerId !== auth.user.id) {
|
|
||||||
throw new BadRequestException('Live photo video does not belong to the user');
|
|
||||||
}
|
|
||||||
if (motionAsset.isVisible) {
|
|
||||||
await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
|
|
||||||
this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, auth.user.id, motionAsset.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const asset = await this.create(auth.user.id, dto, file, sidecarFile);
|
const asset = await this.create(auth.user.id, dto, file, sidecarFile);
|
||||||
@ -289,10 +279,10 @@ export class AssetMediaService {
|
|||||||
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
||||||
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
|
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
|
||||||
const results = await this.assetRepository.getByChecksums(auth.user.id, checksums);
|
const results = await this.assetRepository.getByChecksums(auth.user.id, checksums);
|
||||||
const checksumMap: Record<string, string> = {};
|
const checksumMap: Record<string, { id: string; isTrashed: boolean }> = {};
|
||||||
|
|
||||||
for (const { id, checksum } of results) {
|
for (const { id, deletedAt, checksum } of results) {
|
||||||
checksumMap[checksum.toString('hex')] = id;
|
checksumMap[checksum.toString('hex')] = { id, isTrashed: !!deletedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -301,14 +291,13 @@ export class AssetMediaService {
|
|||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
assetId: duplicate,
|
|
||||||
action: AssetUploadAction.REJECT,
|
action: AssetUploadAction.REJECT,
|
||||||
reason: AssetRejectReason.DUPLICATE,
|
reason: AssetRejectReason.DUPLICATE,
|
||||||
|
assetId: duplicate.id,
|
||||||
|
isTrashed: duplicate.isTrashed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO mime-check
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
action: AssetUploadAction.ACCEPT,
|
action: AssetUploadAction.ACCEPT,
|
||||||
|
@ -39,7 +39,7 @@ import { IStackRepository } from 'src/interfaces/stack.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { requireAccess } from 'src/utils/access';
|
import { requireAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util';
|
import { getAssetFiles, getMyPartnerIds, onBeforeLink } from 'src/utils/asset.util';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
@ -159,6 +159,14 @@ export class AssetService {
|
|||||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||||
|
|
||||||
|
if (rest.livePhotoVideoId) {
|
||||||
|
await onBeforeLink(
|
||||||
|
{ asset: this.assetRepository, event: this.eventRepository },
|
||||||
|
{ userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
||||||
|
|
||||||
await this.assetRepository.update({ id, ...rest });
|
await this.assetRepository.update({ id, ...rest });
|
||||||
|
@ -115,7 +115,7 @@ export class AuditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case PersonPathType.FACE: {
|
case PersonPathType.FACE: {
|
||||||
await this.personRepository.update([{ id, thumbnailPath: pathValue }]);
|
await this.personRepository.update({ id, thumbnailPath: pathValue });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ export class MediaService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.personRepository.update([{ id: person.id, faceAssetId: face.id }]);
|
await this.personRepository.update({ id: person.id, faceAssetId: face.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
|
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
|
||||||
|
@ -8,7 +8,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
@ -220,11 +220,10 @@ describe(MetadataService.name, () => {
|
|||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||||
JobStatus.SUCCESS,
|
JobStatus.SUCCESS,
|
||||||
);
|
);
|
||||||
expect(eventMock.clientSend).toHaveBeenCalledWith(
|
expect(eventMock.emit).toHaveBeenCalledWith('asset.hide', {
|
||||||
ClientEvent.ASSET_HIDDEN,
|
userId: assetStub.livePhotoMotionAsset.ownerId,
|
||||||
assetStub.livePhotoMotionAsset.ownerId,
|
assetId: assetStub.livePhotoMotionAsset.id,
|
||||||
assetStub.livePhotoMotionAsset.id,
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should search by libraryId', async () => {
|
it('should search by libraryId', async () => {
|
||||||
@ -520,6 +519,16 @@ describe(MetadataService.name, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle an invalid Directory Item', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.readTags.mockResolvedValue({
|
||||||
|
MotionPhoto: 1,
|
||||||
|
ContainerDirectory: [{ Foo: 100 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
it('should extract the correct video orientation', async () => {
|
it('should extract the correct video orientation', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
@ -993,13 +1002,12 @@ describe(MetadataService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
metadataMock.readTags.mockResolvedValue(metadataStub.withFaceNoName);
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFaceNoName);
|
||||||
personMock.getDistinctNames.mockResolvedValue([]);
|
personMock.getDistinctNames.mockResolvedValue([]);
|
||||||
personMock.create.mockResolvedValue([]);
|
personMock.createAll.mockResolvedValue([]);
|
||||||
personMock.replaceFaces.mockResolvedValue([]);
|
personMock.replaceFaces.mockResolvedValue([]);
|
||||||
personMock.update.mockResolvedValue([]);
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(personMock.create).toHaveBeenCalledWith([]);
|
expect(personMock.createAll).toHaveBeenCalledWith([]);
|
||||||
expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
|
||||||
expect(personMock.update).toHaveBeenCalledWith([]);
|
expect(personMock.updateAll).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip importing faces with empty name', async () => {
|
it('should skip importing faces with empty name', async () => {
|
||||||
@ -1007,13 +1015,12 @@ describe(MetadataService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
metadataMock.readTags.mockResolvedValue(metadataStub.withFaceEmptyName);
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFaceEmptyName);
|
||||||
personMock.getDistinctNames.mockResolvedValue([]);
|
personMock.getDistinctNames.mockResolvedValue([]);
|
||||||
personMock.create.mockResolvedValue([]);
|
personMock.createAll.mockResolvedValue([]);
|
||||||
personMock.replaceFaces.mockResolvedValue([]);
|
personMock.replaceFaces.mockResolvedValue([]);
|
||||||
personMock.update.mockResolvedValue([]);
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(personMock.create).toHaveBeenCalledWith([]);
|
expect(personMock.createAll).toHaveBeenCalledWith([]);
|
||||||
expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
|
||||||
expect(personMock.update).toHaveBeenCalledWith([]);
|
expect(personMock.updateAll).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply metadata face tags creating new persons', async () => {
|
it('should apply metadata face tags creating new persons', async () => {
|
||||||
@ -1021,13 +1028,13 @@ describe(MetadataService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
|
||||||
personMock.getDistinctNames.mockResolvedValue([]);
|
personMock.getDistinctNames.mockResolvedValue([]);
|
||||||
personMock.create.mockResolvedValue([personStub.withName]);
|
personMock.createAll.mockResolvedValue([personStub.withName.id]);
|
||||||
personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
|
personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
|
||||||
personMock.update.mockResolvedValue([personStub.withName]);
|
personMock.update.mockResolvedValue(personStub.withName);
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
|
||||||
expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||||
expect(personMock.create).toHaveBeenCalledWith([expect.objectContaining({ name: personStub.withName.name })]);
|
expect(personMock.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: personStub.withName.name })]);
|
||||||
expect(personMock.replaceFaces).toHaveBeenCalledWith(
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(
|
||||||
assetStub.primaryImage.id,
|
assetStub.primaryImage.id,
|
||||||
[
|
[
|
||||||
@ -1046,7 +1053,7 @@ describe(MetadataService.name, () => {
|
|||||||
],
|
],
|
||||||
SourceType.EXIF,
|
SourceType.EXIF,
|
||||||
);
|
);
|
||||||
expect(personMock.update).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]);
|
expect(personMock.updateAll).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]);
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||||
@ -1060,13 +1067,13 @@ describe(MetadataService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
|
||||||
personMock.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
personMock.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
||||||
personMock.create.mockResolvedValue([]);
|
personMock.createAll.mockResolvedValue([]);
|
||||||
personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
|
personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
|
||||||
personMock.update.mockResolvedValue([personStub.withName]);
|
personMock.update.mockResolvedValue(personStub.withName);
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
|
||||||
expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||||
expect(personMock.create).toHaveBeenCalledWith([]);
|
expect(personMock.createAll).toHaveBeenCalledWith([]);
|
||||||
expect(personMock.replaceFaces).toHaveBeenCalledWith(
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(
|
||||||
assetStub.primaryImage.id,
|
assetStub.primaryImage.id,
|
||||||
[
|
[
|
||||||
@ -1085,7 +1092,7 @@ describe(MetadataService.name, () => {
|
|||||||
],
|
],
|
||||||
SourceType.EXIF,
|
SourceType.EXIF,
|
||||||
);
|
);
|
||||||
expect(personMock.update).toHaveBeenCalledWith([]);
|
expect(personMock.updateAll).toHaveBeenCalledWith([]);
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([]);
|
expect(jobMock.queueAll).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
IEntityJob,
|
IEntityJob,
|
||||||
@ -186,8 +186,7 @@ export class MetadataService {
|
|||||||
await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
|
await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
|
||||||
await this.albumRepository.removeAsset(motionAsset.id);
|
await this.albumRepository.removeAsset(motionAsset.id);
|
||||||
|
|
||||||
// Notify clients to hide the linked live photo asset
|
await this.eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
|
||||||
this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, motionAsset.ownerId, motionAsset.id);
|
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
@ -428,7 +427,7 @@ export class MetadataService {
|
|||||||
|
|
||||||
if (isMotionPhoto && directory) {
|
if (isMotionPhoto && directory) {
|
||||||
for (const entry of directory) {
|
for (const entry of directory) {
|
||||||
if (entry.Item.Semantic == 'MotionPhoto') {
|
if (entry?.Item?.Semantic == 'MotionPhoto') {
|
||||||
length = entry.Item.Length ?? 0;
|
length = entry.Item.Length ?? 0;
|
||||||
padding = entry.Item.Padding ?? 0;
|
padding = entry.Item.Padding ?? 0;
|
||||||
break;
|
break;
|
||||||
@ -585,18 +584,15 @@ export class MetadataService {
|
|||||||
this.logger.debug(`Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`);
|
this.logger.debug(`Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPersons = await this.personRepository.create(missing);
|
const newPersonIds = await this.personRepository.createAll(missing);
|
||||||
|
|
||||||
const faceIds = await this.personRepository.replaceFaces(asset.id, discoveredFaces, SourceType.EXIF);
|
const faceIds = await this.personRepository.replaceFaces(asset.id, discoveredFaces, SourceType.EXIF);
|
||||||
this.logger.debug(`Created ${faceIds.length} faces for asset ${asset.id}`);
|
this.logger.debug(`Created ${faceIds.length} faces for asset ${asset.id}`);
|
||||||
|
|
||||||
await this.personRepository.update(missingWithFaceAsset);
|
await this.personRepository.updateAll(missingWithFaceAsset);
|
||||||
|
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
newPersons.map((person) => ({
|
newPersonIds.map((id) => ({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } })),
|
||||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
|
||||||
data: { id: person.id },
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,12 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEmit({ event: 'asset.hide' })
|
||||||
|
onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) {
|
||||||
|
// Notify clients to hide the linked live photo asset
|
||||||
|
this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId);
|
||||||
|
}
|
||||||
|
|
||||||
@OnEmit({ event: 'user.signup' })
|
@OnEmit({ event: 'user.signup' })
|
||||||
async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) {
|
async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
|
@ -241,18 +241,18 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update a person's name", async () => {
|
it("should update a person's name", async () => {
|
||||||
personMock.update.mockResolvedValue([personStub.withName]);
|
personMock.update.mockResolvedValue(personStub.withName);
|
||||||
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
|
|
||||||
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
|
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', name: 'Person 1' }]);
|
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' });
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update a person's date of birth", async () => {
|
it("should update a person's date of birth", async () => {
|
||||||
personMock.update.mockResolvedValue([personStub.withBirthDate]);
|
personMock.update.mockResolvedValue(personStub.withBirthDate);
|
||||||
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
|
|
||||||
@ -264,25 +264,25 @@ describe(PersonService.name, () => {
|
|||||||
isHidden: false,
|
isHidden: false,
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
});
|
});
|
||||||
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', birthDate: '1976-06-30' }]);
|
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' });
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update a person visibility', async () => {
|
it('should update a person visibility', async () => {
|
||||||
personMock.update.mockResolvedValue([personStub.withName]);
|
personMock.update.mockResolvedValue(personStub.withName);
|
||||||
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
|
|
||||||
await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
|
await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', isHidden: false }]);
|
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false });
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update a person's thumbnailPath", async () => {
|
it("should update a person's thumbnailPath", async () => {
|
||||||
personMock.update.mockResolvedValue([personStub.withName]);
|
personMock.update.mockResolvedValue(personStub.withName);
|
||||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
@ -291,7 +291,7 @@ describe(PersonService.name, () => {
|
|||||||
sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }),
|
sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }),
|
||||||
).resolves.toEqual(responseDto);
|
).resolves.toEqual(responseDto);
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', faceAssetId: faceStub.face1.id }]);
|
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id });
|
||||||
expect(personMock.getFacesByIds).toHaveBeenCalledWith([
|
expect(personMock.getFacesByIds).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
assetId: faceStub.face1.assetId,
|
assetId: faceStub.face1.assetId,
|
||||||
@ -441,11 +441,11 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
describe('createPerson', () => {
|
describe('createPerson', () => {
|
||||||
it('should create a new person', async () => {
|
it('should create a new person', async () => {
|
||||||
personMock.create.mockResolvedValue([personStub.primaryPerson]);
|
personMock.create.mockResolvedValue(personStub.primaryPerson);
|
||||||
|
|
||||||
await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson);
|
await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson);
|
||||||
|
|
||||||
expect(personMock.create).toHaveBeenCalledWith([{ ownerId: authStub.admin.user.id }]);
|
expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -819,7 +819,7 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue([faceStub.primaryFace1.person]);
|
personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
@ -844,16 +844,14 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue([personStub.withName]);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
expect(personMock.create).toHaveBeenCalledWith([
|
expect(personMock.create).toHaveBeenCalledWith({
|
||||||
{
|
|
||||||
ownerId: faceStub.noPerson1.asset.ownerId,
|
ownerId: faceStub.noPerson1.asset.ownerId,
|
||||||
faceAssetId: faceStub.noPerson1.id,
|
faceAssetId: faceStub.noPerson1.id,
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
expect(personMock.reassignFaces).toHaveBeenCalledWith({
|
expect(personMock.reassignFaces).toHaveBeenCalledWith({
|
||||||
faceIds: [faceStub.noPerson1.id],
|
faceIds: [faceStub.noPerson1.id],
|
||||||
newPersonId: personStub.withName.id,
|
newPersonId: personStub.withName.id,
|
||||||
@ -865,7 +863,7 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue([personStub.withName]);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
@ -884,7 +882,7 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue([personStub.withName]);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
@ -906,7 +904,7 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||||
searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
|
searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue([personStub.withName]);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
|
||||||
|
|
||||||
@ -979,12 +977,10 @@ describe(PersonService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(personMock.update).toHaveBeenCalledWith([
|
expect(personMock.update).toHaveBeenCalledWith({
|
||||||
{
|
|
||||||
id: 'person-1',
|
id: 'person-1',
|
||||||
thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
|
thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a thumbnail without going negative', async () => {
|
it('should generate a thumbnail without going negative', async () => {
|
||||||
@ -1103,7 +1099,7 @@ describe(PersonService.name, () => {
|
|||||||
it('should merge two people with smart merge', async () => {
|
it('should merge two people with smart merge', async () => {
|
||||||
personMock.getById.mockResolvedValueOnce(personStub.randomPerson);
|
personMock.getById.mockResolvedValueOnce(personStub.randomPerson);
|
||||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||||
personMock.update.mockResolvedValue([{ ...personStub.randomPerson, name: personStub.primaryPerson.name }]);
|
personMock.update.mockResolvedValue({ ...personStub.randomPerson, name: personStub.primaryPerson.name });
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3']));
|
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3']));
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||||
|
|
||||||
@ -1116,12 +1112,10 @@ describe(PersonService.name, () => {
|
|||||||
oldPersonId: personStub.primaryPerson.id,
|
oldPersonId: personStub.primaryPerson.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith([
|
expect(personMock.update).toHaveBeenCalledWith({
|
||||||
{
|
|
||||||
id: personStub.randomPerson.id,
|
id: personStub.randomPerson.id,
|
||||||
name: personStub.primaryPerson.name,
|
name: personStub.primaryPerson.name,
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
|
@ -173,7 +173,7 @@ export class PersonService {
|
|||||||
const assetFace = await this.repository.getRandomFace(personId);
|
const assetFace = await this.repository.getRandomFace(personId);
|
||||||
|
|
||||||
if (assetFace !== null) {
|
if (assetFace !== null) {
|
||||||
await this.repository.update([{ id: personId, faceAssetId: assetFace.id }]);
|
await this.repository.update({ id: personId, faceAssetId: assetFace.id });
|
||||||
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } });
|
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,16 +211,13 @@ export class PersonService {
|
|||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
|
create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
|
||||||
const [created] = await this.repository.create([
|
return this.repository.create({
|
||||||
{
|
|
||||||
ownerId: auth.user.id,
|
ownerId: auth.user.id,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
birthDate: dto.birthDate,
|
birthDate: dto.birthDate,
|
||||||
isHidden: dto.isHidden,
|
isHidden: dto.isHidden,
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
return created;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||||
@ -239,7 +236,7 @@ export class PersonService {
|
|||||||
faceId = face.id;
|
faceId = face.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [person] = await this.repository.update([{ id, faceAssetId: faceId, name, birthDate, isHidden }]);
|
const person = await this.repository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
|
||||||
|
|
||||||
if (assetId) {
|
if (assetId) {
|
||||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
||||||
@ -501,7 +498,7 @@ export class PersonService {
|
|||||||
|
|
||||||
if (isCore && !personId) {
|
if (isCore && !personId) {
|
||||||
this.logger.log(`Creating new person for face ${id}`);
|
this.logger.log(`Creating new person for face ${id}`);
|
||||||
const [newPerson] = await this.repository.create([{ ownerId: face.asset.ownerId, faceAssetId: face.id }]);
|
const newPerson = await this.repository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
||||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
|
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
|
||||||
personId = newPerson.id;
|
personId = newPerson.id;
|
||||||
}
|
}
|
||||||
@ -577,7 +574,7 @@ export class PersonService {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);
|
await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);
|
||||||
await this.repository.update([{ id: person.id, thumbnailPath }]);
|
await this.repository.update({ id: person.id, thumbnailPath });
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
@ -624,7 +621,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(update).length > 0) {
|
if (Object.keys(update).length > 0) {
|
||||||
[primaryPerson] = await this.repository.update([{ id: primaryPerson.id, ...update }]);
|
primaryPerson = await this.repository.update({ id: primaryPerson.id, ...update });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeName = mergePerson.name || mergePerson.id;
|
const mergeName = mergePerson.name || mergePerson.id;
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||||
import { AssetFileType, Permission } from 'src/enum';
|
import { AssetFileType, AssetType, Permission } from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
import { checkAccess } from 'src/utils/access';
|
import { checkAccess } from 'src/utils/access';
|
||||||
|
|
||||||
@ -130,3 +133,24 @@ export const getMyPartnerIds = async ({ userId, repository, timelineEnabled }: P
|
|||||||
|
|
||||||
return [...partnerIds];
|
return [...partnerIds];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const onBeforeLink = async (
|
||||||
|
{ asset: assetRepository, event: eventRepository }: { asset: IAssetRepository; event: IEventRepository },
|
||||||
|
{ userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
|
||||||
|
) => {
|
||||||
|
const motionAsset = await assetRepository.getById(livePhotoVideoId);
|
||||||
|
if (!motionAsset) {
|
||||||
|
throw new BadRequestException('Live photo video not found');
|
||||||
|
}
|
||||||
|
if (motionAsset.type !== AssetType.VIDEO) {
|
||||||
|
throw new BadRequestException('Live photo video must be a video');
|
||||||
|
}
|
||||||
|
if (motionAsset.ownerId !== userId) {
|
||||||
|
throw new BadRequestException('Live photo video does not belong to the user');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (motionAsset?.isVisible) {
|
||||||
|
await assetRepository.update({ id: livePhotoVideoId, isVisible: false });
|
||||||
|
await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -13,9 +13,11 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
|
|||||||
getDistinctNames: vitest.fn(),
|
getDistinctNames: vitest.fn(),
|
||||||
|
|
||||||
create: vitest.fn(),
|
create: vitest.fn(),
|
||||||
|
createAll: vitest.fn(),
|
||||||
update: vitest.fn(),
|
update: vitest.fn(),
|
||||||
deleteAll: vitest.fn(),
|
updateAll: vitest.fn(),
|
||||||
delete: vitest.fn(),
|
delete: vitest.fn(),
|
||||||
|
deleteAll: vitest.fn(),
|
||||||
deleteAllFaces: vitest.fn(),
|
deleteAllFaces: vitest.fn(),
|
||||||
|
|
||||||
getStatistics: vitest.fn(),
|
getStatistics: vitest.fn(),
|
||||||
|
13
web/package-lock.json
generated
13
web/package-lock.json
generated
@ -33,7 +33,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.8.0",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^9.0.0",
|
||||||
"@socket.io/component-emitter": "^3.1.0",
|
"@socket.io/component-emitter": "^3.1.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.1",
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
"@sveltejs/enhanced-img": "^0.3.0",
|
"@sveltejs/enhanced-img": "^0.3.0",
|
||||||
@ -746,9 +746,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@faker-js/faker": {
|
"node_modules/@faker-js/faker": {
|
||||||
"version": "8.4.1",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz",
|
||||||
"integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==",
|
"integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -756,9 +756,10 @@
|
|||||||
"url": "https://opencollective.com/fakerjs"
|
"url": "https://opencollective.com/fakerjs"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
"node": ">=18.0.0",
|
||||||
"npm": ">=6.14.13"
|
"npm": ">=9.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/ecma402-abstract": {
|
"node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.8.0",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^9.0.0",
|
||||||
"@socket.io/component-emitter": "^3.1.0",
|
"@socket.io/component-emitter": "^3.1.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.1",
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
"@sveltejs/enhanced-img": "^0.3.0",
|
"@sveltejs/enhanced-img": "^0.3.0",
|
||||||
|
@ -331,8 +331,10 @@
|
|||||||
locale: $locale,
|
locale: $locale,
|
||||||
})
|
})
|
||||||
: DateTime.now()}
|
: DateTime.now()}
|
||||||
|
{@const assetTimeZoneOriginal = asset.exifInfo?.timeZone ?? ''}
|
||||||
<ChangeDate
|
<ChangeDate
|
||||||
initialDate={assetDateTimeOriginal}
|
initialDate={assetDateTimeOriginal}
|
||||||
|
initialTimeZone={assetTimeZoneOriginal}
|
||||||
on:confirm={({ detail: date }) => handleConfirmChangeDate(date)}
|
on:confirm={({ detail: date }) => handleConfirmChangeDate(date)}
|
||||||
on:cancel={() => (isShowChangeDate = false)}
|
on:cancel={() => (isShowChangeDate = false)}
|
||||||
/>
|
/>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
let showAlbumPicker = false;
|
let showAlbumPicker = false;
|
||||||
|
|
||||||
const { getAssets, clearSelect } = getAssetControlContext();
|
const { getAssets } = getAssetControlContext();
|
||||||
|
|
||||||
const handleHideAlbumPicker = () => {
|
const handleHideAlbumPicker = () => {
|
||||||
showAlbumPicker = false;
|
showAlbumPicker = false;
|
||||||
@ -28,7 +28,6 @@
|
|||||||
showAlbumPicker = false;
|
showAlbumPicker = false;
|
||||||
const assetIds = [...getAssets()].map((asset) => asset.id);
|
const assetIds = [...getAssets()].map((asset) => asset.id);
|
||||||
await addAssetsToAlbum(album.id, assetIds);
|
await addAssetsToAlbum(album.id, assetIds);
|
||||||
clearSelect();
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import type { OnLink } from '$lib/utils/actions';
|
||||||
|
import { AssetTypeEnum, updateAsset } from '@immich/sdk';
|
||||||
|
import { mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
|
||||||
|
export let onLink: OnLink;
|
||||||
|
export let menuItem = false;
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
const text = $t('link_motion_video');
|
||||||
|
const icon = mdiMotionPlayOutline;
|
||||||
|
|
||||||
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
|
const handleLink = async () => {
|
||||||
|
let [still, motion] = [...getOwnedAssets()];
|
||||||
|
if (still.type === AssetTypeEnum.Video) {
|
||||||
|
[still, motion] = [motion, still];
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
const response = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: motion.id } });
|
||||||
|
onLink(response);
|
||||||
|
clearSelect();
|
||||||
|
loading = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if menuItem}
|
||||||
|
<MenuOption {text} {icon} onClick={handleLink} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !menuItem}
|
||||||
|
{#if loading}
|
||||||
|
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
||||||
|
{:else}
|
||||||
|
<CircleIconButton title={text} {icon} on:click={handleLink} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
@ -7,6 +7,7 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let initialDate: DateTime = DateTime.now();
|
export let initialDate: DateTime = DateTime.now();
|
||||||
|
export let initialTimeZone: string = '';
|
||||||
|
|
||||||
type ZoneOption = {
|
type ZoneOption = {
|
||||||
/**
|
/**
|
||||||
@ -76,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Find the time zone to select for a given time, date, and offset (e.g. +02:00).
|
* If the time zone is not given, find the timezone to select for a given time, date, and offset (e.g. +02:00).
|
||||||
*
|
*
|
||||||
* This is done so that the list shown to the user includes more helpful names like "Europe/Berlin (+02:00)"
|
* This is done so that the list shown to the user includes more helpful names like "Europe/Berlin (+02:00)"
|
||||||
* instead of just the raw offset or something like "UTC+02:00".
|
* instead of just the raw offset or something like "UTC+02:00".
|
||||||
@ -97,6 +98,7 @@
|
|||||||
) {
|
) {
|
||||||
const offset = date.offset;
|
const offset = date.offset;
|
||||||
const previousSelection = timezones.find((item) => item.value === selectedOption?.value);
|
const previousSelection = timezones.find((item) => item.value === selectedOption?.value);
|
||||||
|
const fromInitialTimeZone = timezones.find((item) => item.value === initialTimeZone);
|
||||||
const sameAsUserTimeZone = timezones.find((item) => item.offsetMinutes === offset && item.value === userTimeZone);
|
const sameAsUserTimeZone = timezones.find((item) => item.offsetMinutes === offset && item.value === userTimeZone);
|
||||||
const firstWithSameOffset = timezones.find((item) => item.offsetMinutes === offset);
|
const firstWithSameOffset = timezones.find((item) => item.offsetMinutes === offset);
|
||||||
const utcFallback = {
|
const utcFallback = {
|
||||||
@ -105,7 +107,7 @@
|
|||||||
value: 'UTC',
|
value: 'UTC',
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
return previousSelection ?? sameAsUserTimeZone ?? firstWithSameOffset ?? utcFallback;
|
return previousSelection ?? fromInitialTimeZone ?? sameAsUserTimeZone ?? firstWithSameOffset ?? utcFallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) {
|
function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) {
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
* - `narrow`: 28rem
|
* - `narrow`: 28rem
|
||||||
* - `auto`: fits the width of the modal content, up to a maximum of 32rem
|
* - `auto`: fits the width of the modal content, up to a maximum of 32rem
|
||||||
*/
|
*/
|
||||||
export let width: 'wide' | 'narrow' | 'auto' = 'narrow';
|
export let width: 'extra-wide' | 'wide' | 'narrow' | 'auto' = 'narrow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique identifier for the modal.
|
* Unique identifier for the modal.
|
||||||
@ -34,14 +34,27 @@
|
|||||||
|
|
||||||
let modalWidth: string;
|
let modalWidth: string;
|
||||||
$: {
|
$: {
|
||||||
if (width === 'wide') {
|
switch (width) {
|
||||||
|
case 'extra-wide': {
|
||||||
|
modalWidth = 'w-[56rem]';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'wide': {
|
||||||
modalWidth = 'w-[48rem]';
|
modalWidth = 'w-[48rem]';
|
||||||
} else if (width === 'narrow') {
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'narrow': {
|
||||||
modalWidth = 'w-[28rem]';
|
modalWidth = 'w-[28rem]';
|
||||||
} else {
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
modalWidth = 'sm:max-w-4xl';
|
modalWidth = 'sm:max-w-4xl';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@ -55,24 +68,28 @@
|
|||||||
use:focusTrap
|
use:focusTrap
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="z-[9999] max-w-[95vw] max-h-[min(95dvh,56rem)] {modalWidth} overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
|
class="z-[9999] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4"
|
||||||
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
|
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby={titleId}
|
aria-labelledby={titleId}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="immich-scrollbar overflow-y-auto max-h-[min(92dvh,64rem)]"
|
||||||
class:scroll-pb-40={isStickyBottom}
|
class:scroll-pb-40={isStickyBottom}
|
||||||
class:sm:scroll-p-24={isStickyBottom}
|
class:sm:scroll-p-24={isStickyBottom}
|
||||||
>
|
>
|
||||||
<ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
|
<ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
|
||||||
<div class="p-5 pt-0">
|
<div class="px-5 pt-0" class:pb-5={isStickyBottom}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{#if isStickyBottom}
|
{#if isStickyBottom}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky bottom-0 py-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500 shadow"
|
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky bottom-0 pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500 shadow z-[9999]"
|
||||||
>
|
>
|
||||||
<slot name="sticky-bottom" />
|
<slot name="sticky-bottom" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -21,12 +21,12 @@
|
|||||||
export let icon: string | undefined = undefined;
|
export let icon: string | undefined = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex place-items-center justify-between px-5 py-3">
|
<div class="flex place-items-center justify-between px-5 pb-3">
|
||||||
<div class="flex gap-2 place-items-center">
|
<div class="flex gap-2 place-items-center">
|
||||||
{#if showLogo}
|
{#if showLogo}
|
||||||
<ImmichLogo noText={true} width={32} />
|
<ImmichLogo noText={true} width={32} />
|
||||||
{:else if icon}
|
{:else if icon}
|
||||||
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
|
<Icon path={icon} size={24} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
<h1 {id}>
|
<h1 {id}>
|
||||||
{title}
|
{title}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import { isSearchEnabled, preventRaceConditionSearchBar, savedSearchTerms } from '$lib/stores/search.store';
|
import { isSearchEnabled, preventRaceConditionSearchBar, savedSearchTerms } from '$lib/stores/search.store';
|
||||||
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
||||||
import SearchHistoryBox from './search-history-box.svelte';
|
import SearchHistoryBox from './search-history-box.svelte';
|
||||||
import SearchFilterBox from './search-filter-box.svelte';
|
import SearchFilterModal from './search-filter-modal.svelte';
|
||||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
@ -160,8 +160,8 @@
|
|||||||
id="main-search-bar"
|
id="main-search-bar"
|
||||||
class="w-full transition-all border-2 px-14 py-4 text-immich-fg/75 dark:text-immich-dark-fg
|
class="w-full transition-all border-2 px-14 py-4 text-immich-fg/75 dark:text-immich-dark-fg
|
||||||
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
|
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
|
||||||
{(showSuggestions && isSearchSuggestions) || showFilter ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
|
{showSuggestions && isSearchSuggestions ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
|
||||||
{$isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
|
{$isSearchEnabled && !showFilter ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
|
||||||
placeholder={$t('search_your_photos')}
|
placeholder={$t('search_your_photos')}
|
||||||
required
|
required
|
||||||
pattern="^(?!m:$).*$"
|
pattern="^(?!m:$).*$"
|
||||||
@ -215,6 +215,6 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if showFilter}
|
{#if showFilter}
|
||||||
<SearchFilterBox {searchQuery} on:search={({ detail }) => onSearch(detail)} />
|
<SearchFilterModal {searchQuery} onSearch={(payload) => onSearch(payload)} onClose={() => (showFilter = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SearchFilter = {
|
export type SearchFilter = {
|
||||||
context?: string;
|
query: string;
|
||||||
filename?: string;
|
queryType: 'smart' | 'metadata';
|
||||||
personIds: Set<string>;
|
personIds: Set<string>;
|
||||||
location: SearchLocationFilter;
|
location: SearchLocationFilter;
|
||||||
camera: SearchCameraFilter;
|
camera: SearchCameraFilter;
|
||||||
@ -24,8 +24,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
|
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
import { fly } from 'svelte/transition';
|
|
||||||
import SearchPeopleSection from './search-people-section.svelte';
|
import SearchPeopleSection from './search-people-section.svelte';
|
||||||
import SearchLocationSection from './search-location-section.svelte';
|
import SearchLocationSection from './search-location-section.svelte';
|
||||||
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
|
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
|
||||||
@ -35,12 +33,17 @@
|
|||||||
import SearchDisplaySection from './search-display-section.svelte';
|
import SearchDisplaySection from './search-display-section.svelte';
|
||||||
import SearchTextSection from './search-text-section.svelte';
|
import SearchTextSection from './search-text-section.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
|
import { mdiTune } from '@mdi/js';
|
||||||
|
import { generateId } from '$lib/utils/generate-id';
|
||||||
|
|
||||||
export let searchQuery: MetadataSearchDto | SmartSearchDto;
|
export let searchQuery: MetadataSearchDto | SmartSearchDto;
|
||||||
|
export let onClose: () => void;
|
||||||
|
export let onSearch: (search: SmartSearchDto | MetadataSearchDto) => void;
|
||||||
|
|
||||||
const parseOptionalDate = (dateString?: string) => (dateString ? parseUtcDate(dateString) : undefined);
|
const parseOptionalDate = (dateString?: string) => (dateString ? parseUtcDate(dateString) : undefined);
|
||||||
const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined;
|
const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined;
|
||||||
const dispatch = createEventDispatcher<{ search: SmartSearchDto | MetadataSearchDto }>();
|
const formId = generateId();
|
||||||
|
|
||||||
// combobox and all the search components have terrible support for value | null so we use empty string instead.
|
// combobox and all the search components have terrible support for value | null so we use empty string instead.
|
||||||
function withNullAsUndefined<T>(value: T | null) {
|
function withNullAsUndefined<T>(value: T | null) {
|
||||||
@ -48,8 +51,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let filter: SearchFilter = {
|
let filter: SearchFilter = {
|
||||||
context: 'query' in searchQuery ? searchQuery.query : '',
|
query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '',
|
||||||
filename: 'originalFileName' in searchQuery ? searchQuery.originalFileName : undefined,
|
queryType: 'query' in searchQuery ? 'smart' : 'metadata',
|
||||||
personIds: new Set('personIds' in searchQuery ? searchQuery.personIds : []),
|
personIds: new Set('personIds' in searchQuery ? searchQuery.personIds : []),
|
||||||
location: {
|
location: {
|
||||||
country: withNullAsUndefined(searchQuery.country),
|
country: withNullAsUndefined(searchQuery.country),
|
||||||
@ -79,6 +82,8 @@
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
filter = {
|
filter = {
|
||||||
|
query: '',
|
||||||
|
queryType: 'smart',
|
||||||
personIds: new Set(),
|
personIds: new Set(),
|
||||||
location: {},
|
location: {},
|
||||||
camera: {},
|
camera: {},
|
||||||
@ -96,9 +101,11 @@
|
|||||||
type = AssetTypeEnum.Video;
|
type = AssetTypeEnum.Video;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = filter.query || undefined;
|
||||||
|
|
||||||
let payload: SmartSearchDto | MetadataSearchDto = {
|
let payload: SmartSearchDto | MetadataSearchDto = {
|
||||||
query: filter.context || undefined,
|
query: filter.queryType === 'smart' ? query : undefined,
|
||||||
originalFileName: filter.filename,
|
originalFileName: filter.queryType === 'metadata' ? query : undefined,
|
||||||
country: filter.location.country,
|
country: filter.location.country,
|
||||||
state: filter.location.state,
|
state: filter.location.state,
|
||||||
city: filter.location.city,
|
city: filter.location.city,
|
||||||
@ -113,26 +120,18 @@
|
|||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch('search', payload);
|
onSearch(payload);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}>
|
||||||
transition:fly={{ y: 25, duration: 250 }}
|
<form id={formId} autocomplete="off" on:submit|preventDefault={search} on:reset|preventDefault={resetForm}>
|
||||||
class="absolute w-full rounded-b-3xl border-2 border-t-0 border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-immich-dark-gray dark:text-gray-300"
|
<div class="space-y-10 pb-10" tabindex="-1">
|
||||||
>
|
|
||||||
<form
|
|
||||||
id="search-filter-form"
|
|
||||||
autocomplete="off"
|
|
||||||
on:submit|preventDefault={search}
|
|
||||||
on:reset|preventDefault={resetForm}
|
|
||||||
>
|
|
||||||
<div class="px-4 sm:px-6 py-4 space-y-10 max-h-[calc(100dvh-12rem)] overflow-y-auto immich-scrollbar" tabindex="-1">
|
|
||||||
<!-- PEOPLE -->
|
<!-- PEOPLE -->
|
||||||
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
|
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
|
||||||
|
|
||||||
<!-- TEXT -->
|
<!-- TEXT -->
|
||||||
<SearchTextSection bind:filename={filter.filename} bind:context={filter.context} />
|
<SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} />
|
||||||
|
|
||||||
<!-- LOCATION -->
|
<!-- LOCATION -->
|
||||||
<SearchLocationSection bind:filters={filter.location} />
|
<SearchLocationSection bind:filters={filter.location} />
|
||||||
@ -143,7 +142,7 @@
|
|||||||
<!-- DATE RANGE -->
|
<!-- DATE RANGE -->
|
||||||
<SearchDateSection bind:filters={filter.date} />
|
<SearchDateSection bind:filters={filter.date} />
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-x-5 gap-y-8">
|
<div class="grid md:grid-cols-2 gap-x-5 gap-y-10">
|
||||||
<!-- MEDIA TYPE -->
|
<!-- MEDIA TYPE -->
|
||||||
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
|
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
|
||||||
|
|
||||||
@ -151,13 +150,10 @@
|
|||||||
<SearchDisplaySection bind:filters={filter.display} />
|
<SearchDisplaySection bind:filters={filter.display} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
id="button-row"
|
|
||||||
class="flex justify-end gap-4 border-t dark:border-gray-800 dark:bg-immich-dark-gray px-4 sm:py-6 py-4 mt-2 rounded-b-3xl"
|
|
||||||
>
|
|
||||||
<Button type="reset" color="gray">{$t('clear_all')}</Button>
|
|
||||||
<Button type="submit">{$t('search')}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
<svelte:fragment slot="sticky-bottom">
|
||||||
|
<Button type="reset" color="gray" fullwidth form={formId}>{$t('clear_all')}</Button>
|
||||||
|
<Button type="submit" fullwidth form={formId}>{$t('search')}</Button>
|
||||||
|
</svelte:fragment>
|
||||||
|
</FullScreenModal>
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
||||||
import { MediaType } from './search-filter-box.svelte';
|
import { MediaType } from './search-filter-modal.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let filteredMedia: MediaType;
|
export let filteredMedia: MediaType;
|
||||||
|
@ -2,46 +2,25 @@
|
|||||||
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let filename: string | undefined;
|
export let query: string | undefined;
|
||||||
export let context: string | undefined;
|
export let queryType: 'smart' | 'metadata' = 'smart';
|
||||||
|
|
||||||
enum TextSearchOptions {
|
|
||||||
Context = 'context',
|
|
||||||
Filename = 'filename',
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedOption = filename ? TextSearchOptions.Filename : TextSearchOptions.Context;
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (selectedOption === TextSearchOptions.Context) {
|
|
||||||
filename = undefined;
|
|
||||||
} else {
|
|
||||||
context = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend class="immich-form-label">{$t('search_type')}</legend>
|
<legend class="immich-form-label">{$t('search_type')}</legend>
|
||||||
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1 mb-2">
|
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1 mb-2">
|
||||||
<RadioButton
|
<RadioButton name="query-type" id="context-radio" label={$t('context')} bind:group={queryType} value="smart" />
|
||||||
name="query-type"
|
|
||||||
id="context-radio"
|
|
||||||
bind:group={selectedOption}
|
|
||||||
label={$t('context')}
|
|
||||||
value={TextSearchOptions.Context}
|
|
||||||
/>
|
|
||||||
<RadioButton
|
<RadioButton
|
||||||
name="query-type"
|
name="query-type"
|
||||||
id="file-name-radio"
|
id="file-name-radio"
|
||||||
bind:group={selectedOption}
|
|
||||||
label={$t('file_name_or_extension')}
|
label={$t('file_name_or_extension')}
|
||||||
value={TextSearchOptions.Filename}
|
bind:group={queryType}
|
||||||
|
value="metadata"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{#if selectedOption === TextSearchOptions.Context}
|
{#if queryType === 'smart'}
|
||||||
<label for="context-input" class="immich-form-label">{$t('search_by_context')}</label>
|
<label for="context-input" class="immich-form-label">{$t('search_by_context')}</label>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input hover:cursor-text w-full !mt-1"
|
class="immich-form-input hover:cursor-text w-full !mt-1"
|
||||||
@ -49,7 +28,7 @@
|
|||||||
id="context-input"
|
id="context-input"
|
||||||
name="context"
|
name="context"
|
||||||
placeholder={$t('sunrise_on_the_beach')}
|
placeholder={$t('sunrise_on_the_beach')}
|
||||||
bind:value={context}
|
bind:value={query}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<label for="file-name-input" class="immich-form-label">{$t('search_by_filename')}</label>
|
<label for="file-name-input" class="immich-form-label">{$t('search_by_filename')}</label>
|
||||||
@ -59,7 +38,7 @@
|
|||||||
id="file-name-input"
|
id="file-name-input"
|
||||||
name="file-name"
|
name="file-name"
|
||||||
placeholder={$t('search_by_filename_example')}
|
placeholder={$t('search_by_filename_example')}
|
||||||
bind:value={filename}
|
bind:value={query}
|
||||||
aria-labelledby="file-name-label"
|
aria-labelledby="file-name-label"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
mdiLoading,
|
mdiLoading,
|
||||||
mdiOpenInNew,
|
mdiOpenInNew,
|
||||||
mdiRestart,
|
mdiRestart,
|
||||||
|
mdiTrashCan,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@ -29,6 +30,10 @@
|
|||||||
uploadAssetsStore.removeItem(uploadAsset.id);
|
uploadAssetsStore.removeItem(uploadAsset.id);
|
||||||
await fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
|
await fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const asLink = (asset: UploadAsset) => {
|
||||||
|
return asset.isTrashed ? `${AppRoute.TRASH}/${asset.assetId}` : `${AppRoute.PHOTOS}/${uploadAsset.assetId}`;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -45,7 +50,11 @@
|
|||||||
{:else if uploadAsset.state === UploadState.ERROR}
|
{:else if uploadAsset.state === UploadState.ERROR}
|
||||||
<Icon path={mdiAlertCircle} size="24" class="text-immich-error" title={$t('error')} />
|
<Icon path={mdiAlertCircle} size="24" class="text-immich-error" title={$t('error')} />
|
||||||
{:else if uploadAsset.state === UploadState.DUPLICATED}
|
{:else if uploadAsset.state === UploadState.DUPLICATED}
|
||||||
|
{#if uploadAsset.isTrashed}
|
||||||
|
<Icon path={mdiTrashCan} size="24" class="text-gray-500" title={$t('asset_skipped_in_trash')} />
|
||||||
|
{:else}
|
||||||
<Icon path={mdiAlertCircle} size="24" class="text-immich-warning" title={$t('asset_skipped')} />
|
<Icon path={mdiAlertCircle} size="24" class="text-immich-warning" title={$t('asset_skipped')} />
|
||||||
|
{/if}
|
||||||
{:else if uploadAsset.state === UploadState.DONE}
|
{:else if uploadAsset.state === UploadState.DONE}
|
||||||
<Icon path={mdiCheckCircle} size="24" class="text-immich-success" title={$t('asset_uploaded')} />
|
<Icon path={mdiCheckCircle} size="24" class="text-immich-success" title={$t('asset_uploaded')} />
|
||||||
{/if}
|
{/if}
|
||||||
@ -56,7 +65,7 @@
|
|||||||
{#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId}
|
{#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId}
|
||||||
<div class="flex items-center justify-between gap-1">
|
<div class="flex items-center justify-between gap-1">
|
||||||
<a
|
<a
|
||||||
href="{AppRoute.PHOTOS}/{uploadAsset.assetId}"
|
href={asLink(uploadAsset)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class=""
|
class=""
|
||||||
|
@ -387,6 +387,7 @@
|
|||||||
"asset_offline": "Asset offline",
|
"asset_offline": "Asset offline",
|
||||||
"asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.",
|
"asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.",
|
||||||
"asset_skipped": "Skipped",
|
"asset_skipped": "Skipped",
|
||||||
|
"asset_skipped_in_trash": "In trash",
|
||||||
"asset_uploaded": "Uploaded",
|
"asset_uploaded": "Uploaded",
|
||||||
"asset_uploading": "Uploading...",
|
"asset_uploading": "Uploading...",
|
||||||
"assets": "Assets",
|
"assets": "Assets",
|
||||||
@ -784,6 +785,7 @@
|
|||||||
"library_options": "Library options",
|
"library_options": "Library options",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"like_deleted": "Like deleted",
|
"like_deleted": "Like deleted",
|
||||||
|
"link_motion_video": "Link motion video",
|
||||||
"link_options": "Link options",
|
"link_options": "Link options",
|
||||||
"link_to_oauth": "Link to OAuth",
|
"link_to_oauth": "Link to OAuth",
|
||||||
"linked_oauth_account": "Linked OAuth account",
|
"linked_oauth_account": "Linked OAuth account",
|
||||||
@ -1067,6 +1069,7 @@
|
|||||||
"search_for_existing_person": "Search for existing person",
|
"search_for_existing_person": "Search for existing person",
|
||||||
"search_no_people": "No people",
|
"search_no_people": "No people",
|
||||||
"search_no_people_named": "No people named \"{name}\"",
|
"search_no_people_named": "No people named \"{name}\"",
|
||||||
|
"search_options": "Search options",
|
||||||
"search_people": "Search people",
|
"search_people": "Search people",
|
||||||
"search_places": "Search places",
|
"search_places": "Search places",
|
||||||
"search_state": "Search state...",
|
"search_state": "Search state...",
|
||||||
|
@ -10,6 +10,7 @@ export type UploadAsset = {
|
|||||||
id: string;
|
id: string;
|
||||||
file: File;
|
file: File;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
|
isTrashed?: boolean;
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
state?: UploadState;
|
state?: UploadState;
|
||||||
|
@ -6,6 +6,7 @@ import { handleError } from './handle-error';
|
|||||||
|
|
||||||
export type OnDelete = (assetIds: string[]) => void;
|
export type OnDelete = (assetIds: string[]) => void;
|
||||||
export type OnRestore = (ids: string[]) => void;
|
export type OnRestore = (ids: string[]) => void;
|
||||||
|
export type OnLink = (asset: AssetResponseDto) => void;
|
||||||
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
||||||
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
||||||
export type OnStack = (ids: string[]) => void;
|
export type OnStack = (ids: string[]) => void;
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { getServerErrorMessage, handleError } from './handle-error';
|
import { handleError } from './handle-error';
|
||||||
|
|
||||||
export const addDummyItems = () => {
|
export const addDummyItems = () => {
|
||||||
uploadAssetsStore.addItem({ id: 'asset-0', file: { name: 'asset0.jpg', size: 123_456 } as File });
|
uploadAssetsStore.addItem({ id: 'asset-0', file: { name: 'asset0.jpg', size: 123_456 } as File });
|
||||||
@ -28,7 +28,9 @@ export const addDummyItems = () => {
|
|||||||
uploadAssetsStore.addItem({ id: 'asset-3', file: { name: 'asset3.jpg', size: 123_456 } as File });
|
uploadAssetsStore.addItem({ id: 'asset-3', file: { name: 'asset3.jpg', size: 123_456 } as File });
|
||||||
uploadAssetsStore.updateItem('asset-3', { state: UploadState.DUPLICATED, assetId: 'asset-2' });
|
uploadAssetsStore.updateItem('asset-3', { state: UploadState.DUPLICATED, assetId: 'asset-2' });
|
||||||
uploadAssetsStore.addItem({ id: 'asset-4', file: { name: 'asset3.jpg', size: 123_456 } as File });
|
uploadAssetsStore.addItem({ id: 'asset-4', file: { name: 'asset3.jpg', size: 123_456 } as File });
|
||||||
uploadAssetsStore.updateItem('asset-4', { state: UploadState.DONE });
|
uploadAssetsStore.updateItem('asset-4', { state: UploadState.DUPLICATED, assetId: 'asset-2', isTrashed: true });
|
||||||
|
uploadAssetsStore.addItem({ id: 'asset-10', file: { name: 'asset3.jpg', size: 123_456 } as File });
|
||||||
|
uploadAssetsStore.updateItem('asset-10', { state: UploadState.DONE });
|
||||||
uploadAssetsStore.track('error');
|
uploadAssetsStore.track('error');
|
||||||
uploadAssetsStore.track('success');
|
uploadAssetsStore.track('success');
|
||||||
uploadAssetsStore.track('duplicate');
|
uploadAssetsStore.track('duplicate');
|
||||||
@ -122,7 +124,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
|
|||||||
formData.append(key, value);
|
formData.append(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let responseData: AssetMediaResponseDto | undefined;
|
let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
|
||||||
const key = getKey();
|
const key = getKey();
|
||||||
if (crypto?.subtle?.digest && !key) {
|
if (crypto?.subtle?.digest && !key) {
|
||||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
||||||
@ -138,7 +140,11 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
|
|||||||
results: [checkUploadResult],
|
results: [checkUploadResult],
|
||||||
} = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: assetFile.name, checksum }] } });
|
} = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: assetFile.name, checksum }] } });
|
||||||
if (checkUploadResult.action === Action.Reject && checkUploadResult.assetId) {
|
if (checkUploadResult.action === Action.Reject && checkUploadResult.assetId) {
|
||||||
responseData = { status: AssetMediaStatus.Duplicate, id: checkUploadResult.assetId };
|
responseData = {
|
||||||
|
status: AssetMediaStatus.Duplicate,
|
||||||
|
id: checkUploadResult.assetId,
|
||||||
|
isTrashed: checkUploadResult.isTrashed,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error calculating sha1 file=${assetFile.name})`, error);
|
console.error(`Error calculating sha1 file=${assetFile.name})`, error);
|
||||||
@ -185,6 +191,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
|
|||||||
uploadAssetsStore.updateItem(deviceAssetId, {
|
uploadAssetsStore.updateItem(deviceAssetId, {
|
||||||
state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE,
|
state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE,
|
||||||
assetId: responseData.id,
|
assetId: responseData.id,
|
||||||
|
isTrashed: responseData.isTrashed,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (responseData.status !== AssetMediaStatus.Duplicate) {
|
if (responseData.status !== AssetMediaStatus.Duplicate) {
|
||||||
@ -195,10 +202,9 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
|
|||||||
|
|
||||||
return responseData.id;
|
return responseData.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_upload_file'));
|
const errorMessage = handleError(error, $t('errors.unable_to_upload_file'));
|
||||||
const reason = getServerErrorMessage(error) || error;
|
|
||||||
uploadAssetsStore.track('error');
|
uploadAssetsStore.track('error');
|
||||||
uploadAssetsStore.updateItem(deviceAssetId, { state: UploadState.ERROR, error: reason });
|
uploadAssetsStore.updateItem(deviceAssetId, { state: UploadState.ERROR, error: errorMessage });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,13 @@ export function handleError(error: unknown, message: string) {
|
|||||||
serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`;
|
serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationController.show({
|
const errorMessage = serverMessage || message;
|
||||||
message: serverMessage || message,
|
|
||||||
type: NotificationType.Error,
|
notificationController.show({ message: errorMessage, type: NotificationType.Error });
|
||||||
});
|
|
||||||
|
return errorMessage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@
|
|||||||
const assetInteractionStore = createAssetInteractionStore();
|
const assetInteractionStore = createAssetInteractionStore();
|
||||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||||
|
|
||||||
$: timelineStore = new AssetStore({ isArchived: false }, albumId);
|
$: timelineStore = new AssetStore({ isArchived: false, withPartners: true }, albumId);
|
||||||
const timelineInteractionStore = createAssetInteractionStore();
|
const timelineInteractionStore = createAssetInteractionStore();
|
||||||
const { selectedAssets: timelineSelected } = timelineInteractionStore;
|
const { selectedAssets: timelineSelected } = timelineInteractionStore;
|
||||||
|
|
||||||
|
@ -2,30 +2,32 @@
|
|||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
|
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
||||||
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
|
||||||
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
|
||||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||||
import StackAction from '$lib/components/photos-page/actions/stack-action.svelte';
|
import LinkLivePhotoAction from '$lib/components/photos-page/actions/link-live-photo-action.svelte';
|
||||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||||
|
import StackAction from '$lib/components/photos-page/actions/stack-action.svelte';
|
||||||
|
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
||||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
import MemoryLane from '$lib/components/photos-page/memory-lane.svelte';
|
import MemoryLane from '$lib/components/photos-page/memory-lane.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||||
import { AssetStore } from '$lib/stores/assets.store';
|
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
import { AssetStore } from '$lib/stores/assets.store';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import { t } from 'svelte-i18n';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
|
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true });
|
const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true });
|
||||||
@ -51,6 +53,13 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLink = (asset: AssetResponseDto) => {
|
||||||
|
if (asset.livePhotoVideoId) {
|
||||||
|
assetStore.removeAssets([asset.livePhotoVideoId]);
|
||||||
|
}
|
||||||
|
assetStore.updateAssets([asset]);
|
||||||
|
};
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
assetStore.destroy();
|
assetStore.destroy();
|
||||||
});
|
});
|
||||||
@ -78,6 +87,9 @@
|
|||||||
onUnstack={(assets) => assetStore.addAssets(assets)}
|
onUnstack={(assets) => assetStore.addAssets(assets)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $selectedAssets.size === 2 && [...$selectedAssets].some((asset) => asset.type === AssetTypeEnum.Image && [...$selectedAssets].some((asset) => asset.type === AssetTypeEnum.Video))}
|
||||||
|
<LinkLivePhotoAction menuItem onLink={handleLink} />
|
||||||
|
{/if}
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
<ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
|
<ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user