mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
Merge branch 'main' into service_worker_appstatic
This commit is contained in:
commit
b6e1c00b29
@ -5,22 +5,6 @@ import { app, asBearerAuth, utils } from 'src/utils';
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const invalidBirthday = [
|
|
||||||
{
|
|
||||||
birthDate: 'false',
|
|
||||||
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
birthDate: '123567',
|
|
||||||
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
birthDate: 123_567,
|
|
||||||
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
|
|
||||||
},
|
|
||||||
{ birthDate: '9999-01-01', response: ['Birth date cannot be in the future'] },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('/people', () => {
|
describe('/people', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let visiblePerson: PersonResponseDto;
|
let visiblePerson: PersonResponseDto;
|
||||||
@ -58,14 +42,6 @@ describe('/people', () => {
|
|||||||
|
|
||||||
describe('GET /people', () => {
|
describe('GET /people', () => {
|
||||||
beforeEach(async () => {});
|
beforeEach(async () => {});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/people');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all people (including hidden)', async () => {
|
it('should return all people (including hidden)', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/people')
|
.get('/people')
|
||||||
@ -117,13 +93,6 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /people/:id', () => {
|
describe('GET /people/:id', () => {
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(`/people/${uuidDto.notFound}`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error if person with id does not exist', async () => {
|
it('should throw error if person with id does not exist', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/people/${uuidDto.notFound}`)
|
.get(`/people/${uuidDto.notFound}`)
|
||||||
@ -144,13 +113,6 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /people/:id/statistics', () => {
|
describe('GET /people/:id/statistics', () => {
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(`/people/${multipleAssetsPerson.id}/statistics`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error if person with id does not exist', async () => {
|
it('should throw error if person with id does not exist', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/people/${uuidDto.notFound}/statistics`)
|
.get(`/people/${uuidDto.notFound}/statistics`)
|
||||||
@ -171,23 +133,6 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /people', () => {
|
describe('POST /people', () => {
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post(`/people`);
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const { birthDate, response } of invalidBirthday) {
|
|
||||||
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post(`/people`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ birthDate });
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(response));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should create a person', async () => {
|
it('should create a person', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/people`)
|
.post(`/people`)
|
||||||
@ -223,39 +168,6 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /people/:id', () => {
|
describe('PUT /people/:id', () => {
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).put(`/people/${uuidDto.notFound}`);
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const { key, type } of [
|
|
||||||
{ key: 'name', type: 'string' },
|
|
||||||
{ key: 'featureFaceAssetId', type: 'string' },
|
|
||||||
{ key: 'isHidden', type: 'boolean value' },
|
|
||||||
{ key: 'isFavorite', type: 'boolean value' },
|
|
||||||
]) {
|
|
||||||
it(`should not allow null ${key}`, async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/people/${visiblePerson.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ [key]: null });
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest([`${key} must be a ${type}`]));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { birthDate, response } of invalidBirthday) {
|
|
||||||
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/people/${visiblePerson.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ birthDate });
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(response));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should update a date of birth', async () => {
|
it('should update a date of birth', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/people/${visiblePerson.id}`)
|
.put(`/people/${visiblePerson.id}`)
|
||||||
@ -312,12 +224,6 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /people/:id/merge', () => {
|
describe('POST /people/:id/merge', () => {
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post(`/people/${uuidDto.notFound}/merge`);
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not supporting merging a person into themselves', async () => {
|
it('should not supporting merging a person into themselves', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/people/${visiblePerson.id}/merge`)
|
.post(`/people/${visiblePerson.id}/merge`)
|
||||||
|
@ -601,6 +601,7 @@
|
|||||||
"cannot_undo_this_action": "You cannot undo this action!",
|
"cannot_undo_this_action": "You cannot undo this action!",
|
||||||
"cannot_update_the_description": "Cannot update the description",
|
"cannot_update_the_description": "Cannot update the description",
|
||||||
"change_date": "Change date",
|
"change_date": "Change date",
|
||||||
|
"change_description": "Change description",
|
||||||
"change_display_order": "Change display order",
|
"change_display_order": "Change display order",
|
||||||
"change_expiration_time": "Change expiration time",
|
"change_expiration_time": "Change expiration time",
|
||||||
"change_location": "Change location",
|
"change_location": "Change location",
|
||||||
@ -794,6 +795,8 @@
|
|||||||
"edit_avatar": "Edit avatar",
|
"edit_avatar": "Edit avatar",
|
||||||
"edit_date": "Edit date",
|
"edit_date": "Edit date",
|
||||||
"edit_date_and_time": "Edit date and time",
|
"edit_date_and_time": "Edit date and time",
|
||||||
|
"edit_description": "Edit description",
|
||||||
|
"edit_description_prompt": "Please select a new description:",
|
||||||
"edit_exclusion_pattern": "Edit exclusion pattern",
|
"edit_exclusion_pattern": "Edit exclusion pattern",
|
||||||
"edit_faces": "Edit faces",
|
"edit_faces": "Edit faces",
|
||||||
"edit_import_path": "Edit import path",
|
"edit_import_path": "Edit import path",
|
||||||
@ -882,6 +885,7 @@
|
|||||||
"unable_to_archive_unarchive": "Unable to {archived, select, true {archive} other {unarchive}}",
|
"unable_to_archive_unarchive": "Unable to {archived, select, true {archive} other {unarchive}}",
|
||||||
"unable_to_change_album_user_role": "Unable to change the album user's role",
|
"unable_to_change_album_user_role": "Unable to change the album user's role",
|
||||||
"unable_to_change_date": "Unable to change date",
|
"unable_to_change_date": "Unable to change date",
|
||||||
|
"unable_to_change_description": "Unable to change description",
|
||||||
"unable_to_change_favorite": "Unable to change favorite for asset",
|
"unable_to_change_favorite": "Unable to change favorite for asset",
|
||||||
"unable_to_change_location": "Unable to change location",
|
"unable_to_change_location": "Unable to change location",
|
||||||
"unable_to_change_password": "Unable to change password",
|
"unable_to_change_password": "Unable to change password",
|
||||||
|
@ -8,10 +8,13 @@ FROM builder-cpu AS builder-cuda
|
|||||||
|
|
||||||
FROM builder-cpu AS builder-armnn
|
FROM builder-cpu AS builder-armnn
|
||||||
|
|
||||||
|
# renovate: datasource=github-releases depName=ARM-software/armnn
|
||||||
|
ARG ARMNN_VERSION="v24.05"
|
||||||
|
|
||||||
ENV ARMNN_PATH=/opt/armnn
|
ENV ARMNN_PATH=/opt/armnn
|
||||||
COPY ann /opt/ann
|
COPY ann /opt/ann
|
||||||
RUN mkdir /opt/armnn && \
|
RUN mkdir /opt/armnn && \
|
||||||
curl -SL "https://github.com/ARM-software/armnn/releases/download/v24.05/ArmNN-linux-aarch64.tar.gz" | tar -zx -C /opt/armnn && \
|
curl -SL "https://github.com/ARM-software/armnn/releases/download/${ARMNN_VERSION}/ArmNN-linux-aarch64.tar.gz" | tar -zx -C /opt/armnn && \
|
||||||
cd /opt/ann && \
|
cd /opt/ann && \
|
||||||
sh build.sh
|
sh build.sh
|
||||||
|
|
||||||
@ -21,6 +24,8 @@ FROM builder-cpu AS builder-rknn
|
|||||||
# TODO: find a way to reduce the image size
|
# TODO: find a way to reduce the image size
|
||||||
FROM rocm/dev-ubuntu-22.04:6.3.4-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS builder-rocm
|
FROM rocm/dev-ubuntu-22.04:6.3.4-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS builder-rocm
|
||||||
|
|
||||||
|
# renovate: datasource=github-releases depName=Microsoft/onnxruntime
|
||||||
|
ARG ONNXRUNTIME_VERSION="v1.20.1"
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends wget git python3.10-venv
|
RUN apt-get update && apt-get install -y --no-install-recommends wget git python3.10-venv
|
||||||
@ -32,7 +37,7 @@ RUN wget -nv https://github.com/Kitware/CMake/releases/download/v3.30.1/cmake-3.
|
|||||||
|
|
||||||
ENV PATH=/code/cmake-3.30.1-linux-x86_64/bin:${PATH}
|
ENV PATH=/code/cmake-3.30.1-linux-x86_64/bin:${PATH}
|
||||||
|
|
||||||
RUN git clone --single-branch --branch v1.20.1 --recursive "https://github.com/Microsoft/onnxruntime" onnxruntime
|
RUN git clone --single-branch --branch "${ONNXRUNTIME_VERSION}" --recursive "https://github.com/Microsoft/onnxruntime" onnxruntime
|
||||||
WORKDIR /code/onnxruntime
|
WORKDIR /code/onnxruntime
|
||||||
# Fix for multi-threading based on comments in https://github.com/microsoft/onnxruntime/pull/19567
|
# Fix for multi-threading based on comments in https://github.com/microsoft/onnxruntime/pull/19567
|
||||||
# TODO: find a way to fix this without disabling algo caching
|
# TODO: find a way to fix this without disabling algo caching
|
||||||
@ -42,7 +47,7 @@ RUN git apply /tmp/*.patch
|
|||||||
RUN /bin/sh ./dockerfiles/scripts/install_common_deps.sh
|
RUN /bin/sh ./dockerfiles/scripts/install_common_deps.sh
|
||||||
# Note: the `parallel` setting uses a substantial amount of RAM
|
# Note: the `parallel` setting uses a substantial amount of RAM
|
||||||
RUN ./build.sh --allow_running_as_root --config Release --build_wheel --update --build --parallel 17 --cmake_extra_defines\
|
RUN ./build.sh --allow_running_as_root --config Release --build_wheel --update --build --parallel 17 --cmake_extra_defines\
|
||||||
ONNXRUNTIME_VERSION=1.20.1 --skip_tests --use_rocm --rocm_home=/opt/rocm
|
ONNXRUNTIME_VERSION="${ONNXRUNTIME_VERSION}" --skip_tests --use_rocm --rocm_home=/opt/rocm
|
||||||
RUN mv /code/onnxruntime/build/Linux/Release/dist/*.whl /opt/
|
RUN mv /code/onnxruntime/build/Linux/Release/dist/*.whl /opt/
|
||||||
|
|
||||||
FROM builder-${DEVICE} AS builder
|
FROM builder-${DEVICE} AS builder
|
||||||
@ -74,6 +79,7 @@ RUN apt-get update && \
|
|||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-core_1.0.17384.11_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-core_1.0.17384.11_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-opencl_1.0.17384.11_amd64.deb && \
|
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-opencl_1.0.17384.11_amd64.deb && \
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/intel-opencl-icd_24.31.30508.7_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/intel-opencl-icd_24.31.30508.7_amd64.deb && \
|
||||||
|
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
|
||||||
wget -nv https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/libigdgmm12_22.4.1_amd64.deb && \
|
wget -nv https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/libigdgmm12_22.4.1_amd64.deb && \
|
||||||
dpkg -i *.deb && \
|
dpkg -i *.deb && \
|
||||||
rm *.deb && \
|
rm *.deb && \
|
||||||
@ -118,9 +124,12 @@ COPY --from=builder-armnn \
|
|||||||
|
|
||||||
FROM prod-cpu AS prod-rknn
|
FROM prod-cpu AS prod-rknn
|
||||||
|
|
||||||
|
# renovate: datasource=github-tags depName=airockchip/rknn-toolkit2
|
||||||
|
ARG RKNN_TOOLKIT_VERSION="v2.3.0"
|
||||||
|
|
||||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||||
|
|
||||||
ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/v2.3.0/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib/
|
ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
|
||||||
|
|
||||||
FROM prod-${DEVICE} AS prod
|
FROM prod-${DEVICE} AS prod
|
||||||
|
|
||||||
|
19
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
19
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@ -14,6 +14,7 @@ class AssetBulkUpdateDto {
|
|||||||
/// Returns a new [AssetBulkUpdateDto] instance.
|
/// Returns a new [AssetBulkUpdateDto] instance.
|
||||||
AssetBulkUpdateDto({
|
AssetBulkUpdateDto({
|
||||||
this.dateTimeOriginal,
|
this.dateTimeOriginal,
|
||||||
|
this.description,
|
||||||
this.duplicateId,
|
this.duplicateId,
|
||||||
this.ids = const [],
|
this.ids = const [],
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
@ -31,6 +32,14 @@ class AssetBulkUpdateDto {
|
|||||||
///
|
///
|
||||||
String? dateTimeOriginal;
|
String? dateTimeOriginal;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? description;
|
||||||
|
|
||||||
String? duplicateId;
|
String? duplicateId;
|
||||||
|
|
||||||
List<String> ids;
|
List<String> ids;
|
||||||
@ -80,6 +89,7 @@ class AssetBulkUpdateDto {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
|
other.description == description &&
|
||||||
other.duplicateId == duplicateId &&
|
other.duplicateId == duplicateId &&
|
||||||
_deepEquality.equals(other.ids, ids) &&
|
_deepEquality.equals(other.ids, ids) &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
@ -92,6 +102,7 @@ class AssetBulkUpdateDto {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
||||||
|
(description == null ? 0 : description!.hashCode) +
|
||||||
(duplicateId == null ? 0 : duplicateId!.hashCode) +
|
(duplicateId == null ? 0 : duplicateId!.hashCode) +
|
||||||
(ids.hashCode) +
|
(ids.hashCode) +
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
@ -101,7 +112,7 @@ class AssetBulkUpdateDto {
|
|||||||
(visibility == null ? 0 : visibility!.hashCode);
|
(visibility == null ? 0 : visibility!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]';
|
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, description=$description, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -110,6 +121,11 @@ class AssetBulkUpdateDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'dateTimeOriginal'] = null;
|
// json[r'dateTimeOriginal'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.description != null) {
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
} else {
|
||||||
|
// json[r'description'] = null;
|
||||||
|
}
|
||||||
if (this.duplicateId != null) {
|
if (this.duplicateId != null) {
|
||||||
json[r'duplicateId'] = this.duplicateId;
|
json[r'duplicateId'] = this.duplicateId;
|
||||||
} else {
|
} else {
|
||||||
@ -154,6 +170,7 @@ class AssetBulkUpdateDto {
|
|||||||
|
|
||||||
return AssetBulkUpdateDto(
|
return AssetBulkUpdateDto(
|
||||||
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
||||||
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||||
ids: json[r'ids'] is Iterable
|
ids: json[r'ids'] is Iterable
|
||||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
@ -8605,6 +8605,9 @@
|
|||||||
"dateTimeOriginal": {
|
"dateTimeOriginal": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"duplicateId": {
|
"duplicateId": {
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -11075,6 +11078,7 @@
|
|||||||
},
|
},
|
||||||
"featureFaceAssetId": {
|
"featureFaceAssetId": {
|
||||||
"description": "Asset is used to get the feature face thumbnail.",
|
"description": "Asset is used to get the feature face thumbnail.",
|
||||||
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
@ -11280,6 +11284,7 @@
|
|||||||
},
|
},
|
||||||
"featureFaceAssetId": {
|
"featureFaceAssetId": {
|
||||||
"description": "Asset is used to get the feature face thumbnail.",
|
"description": "Asset is used to get the feature face thumbnail.",
|
||||||
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isFavorite": {
|
"isFavorite": {
|
||||||
|
@ -431,6 +431,7 @@ export type AssetMediaResponseDto = {
|
|||||||
};
|
};
|
||||||
export type AssetBulkUpdateDto = {
|
export type AssetBulkUpdateDto = {
|
||||||
dateTimeOriginal?: string;
|
dateTimeOriginal?: string;
|
||||||
|
description?: string;
|
||||||
duplicateId?: string | null;
|
duplicateId?: string | null;
|
||||||
ids: string[];
|
ids: string[];
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
|
172
server/src/controllers/person.controller.spec.ts
Normal file
172
server/src/controllers/person.controller.spec.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { PersonService } from 'src/services/person.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { errorDto } from 'test/medium/responses';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(PersonController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(PersonService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(PersonController, [
|
||||||
|
{ provide: PersonService, useValue: service },
|
||||||
|
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
|
||||||
|
]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /people', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get('/people');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should require closestPersonId to be a uuid`, async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.get(`/people`)
|
||||||
|
.query({ closestPersonId: 'invalid' })
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should require closestAssetId to be a uuid`, async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.get(`/people`)
|
||||||
|
.query({ closestAssetId: 'invalid' })
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /people', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/people');
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map an empty birthDate to null', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' });
|
||||||
|
expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /people/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /people/:id', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).put(`/people/123`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should not allow a null name`, async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.post(`/people`)
|
||||||
|
.send({ name: null })
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['name must be a string']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should require featureFaceAssetId to be a uuid`, async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/people/${factory.uuid()}`)
|
||||||
|
.send({ featureFaceAssetId: 'invalid' })
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['featureFaceAssetId must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should require isFavorite to be a boolean`, async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/people/${factory.uuid()}`)
|
||||||
|
.send({ isFavorite: 'invalid' })
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should require isHidden to be a boolean`, async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/people/${factory.uuid()}`)
|
||||||
|
.send({ isHidden: 'invalid' })
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['isHidden must be a boolean value']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map an empty birthDate to null', async () => {
|
||||||
|
const id = factory.uuid();
|
||||||
|
await request(ctx.getHttpServer()).put(`/people/${id}`).send({ birthDate: '' });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, id, { birthDate: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accept an invalid birth date (false)', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/people/${factory.uuid()}`)
|
||||||
|
.send({ birthDate: false });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest([
|
||||||
|
'birthDate must be a string in the format yyyy-MM-dd',
|
||||||
|
'Birth date cannot be in the future',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accept an invalid birth date (number)', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/people/${factory.uuid()}`)
|
||||||
|
.send({ birthDate: 123_456 });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
errorDto.badRequest([
|
||||||
|
'birthDate must be a string in the format yyyy-MM-dd',
|
||||||
|
'Birth date cannot be in the future',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accept a birth date in the future)', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.put(`/people/${factory.uuid()}`)
|
||||||
|
.send({ birthDate: '9999-01-01' });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['Birth date cannot be in the future']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /people/:id/merge', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /people/:id/statistics', () => {
|
||||||
|
it('should be an authenticated route', async () => {
|
||||||
|
await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}/statistics`);
|
||||||
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -27,7 +27,9 @@ export class PersonController {
|
|||||||
constructor(
|
constructor(
|
||||||
private service: PersonService,
|
private service: PersonService,
|
||||||
private logger: LoggingRepository,
|
private logger: LoggingRepository,
|
||||||
) {}
|
) {
|
||||||
|
this.logger.setContext(PersonController.name);
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Authenticated({ permission: Permission.PERSON_READ })
|
@Authenticated({ permission: Permission.PERSON_READ })
|
||||||
|
@ -54,6 +54,10 @@ export class UpdateAssetBase {
|
|||||||
@Max(5)
|
@Max(5)
|
||||||
@Min(-1)
|
@Min(-1)
|
||||||
rating?: number;
|
rating?: number;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||||
@ -65,10 +69,6 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateAssetDto extends UpdateAssetBase {
|
export class UpdateAssetDto extends UpdateAssetBase {
|
||||||
@Optional()
|
|
||||||
@IsString()
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
@ValidateUUID({ optional: true, nullable: true })
|
@ValidateUUID({ optional: true, nullable: true })
|
||||||
livePhotoVideoId?: string | null;
|
livePhotoVideoId?: string | null;
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ export class PersonCreateDto {
|
|||||||
@ApiProperty({ format: 'date' })
|
@ApiProperty({ format: 'date' })
|
||||||
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
||||||
@IsDateStringFormat('yyyy-MM-dd')
|
@IsDateStringFormat('yyyy-MM-dd')
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
birthDate?: Date | null;
|
birthDate?: Date | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,8 +54,7 @@ export class PersonUpdateDto extends PersonCreateDto {
|
|||||||
/**
|
/**
|
||||||
* Asset is used to get the feature face thumbnail.
|
* Asset is used to get the feature face thumbnail.
|
||||||
*/
|
*/
|
||||||
@Optional()
|
@ValidateUUID({ optional: true })
|
||||||
@IsString()
|
|
||||||
featureFaceAssetId?: string;
|
featureFaceAssetId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,6 +567,7 @@ export enum DatabaseLock {
|
|||||||
Library = 1337,
|
Library = 1337,
|
||||||
GetSystemConfig = 69,
|
GetSystemConfig = 69,
|
||||||
BackupDatabase = 42,
|
BackupDatabase = 42,
|
||||||
|
MemoryCreation = 777,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SyncRequestType {
|
export enum SyncRequestType {
|
||||||
|
@ -108,13 +108,21 @@ export class AssetService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||||
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
const { ids, description, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
||||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
|
||||||
|
|
||||||
if (dateTimeOriginal !== undefined || latitude !== undefined || longitude !== undefined) {
|
if (
|
||||||
await this.assetRepository.updateAllExif(ids, { dateTimeOriginal, latitude, longitude });
|
description !== undefined ||
|
||||||
|
dateTimeOriginal !== undefined ||
|
||||||
|
latitude !== undefined ||
|
||||||
|
longitude !== undefined
|
||||||
|
) {
|
||||||
|
await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude });
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
ids.map((id) => ({ name: JobName.SIDECAR_WRITE, data: { id, dateTimeOriginal, latitude, longitude } })),
|
ids.map((id) => ({
|
||||||
|
name: JobName.SIDECAR_WRITE,
|
||||||
|
data: { id, description, dateTimeOriginal, latitude, longitude },
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,9 +4,8 @@ import { OnJob } from 'src/decorators';
|
|||||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
|
import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
|
||||||
import { JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum';
|
import { DatabaseLock, JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { OnThisDayData } from 'src/types';
|
|
||||||
import { addAssets, getMyPartnerIds, removeAssets } from 'src/utils/asset.util';
|
import { addAssets, getMyPartnerIds, removeAssets } from 'src/utils/asset.util';
|
||||||
|
|
||||||
const DAYS = 3;
|
const DAYS = 3;
|
||||||
@ -16,19 +15,19 @@ export class MemoryService extends BaseService {
|
|||||||
@OnJob({ name: JobName.MEMORIES_CREATE, queue: QueueName.BACKGROUND_TASK })
|
@OnJob({ name: JobName.MEMORIES_CREATE, queue: QueueName.BACKGROUND_TASK })
|
||||||
async onMemoriesCreate() {
|
async onMemoriesCreate() {
|
||||||
const users = await this.userRepository.getList({ withDeleted: false });
|
const users = await this.userRepository.getList({ withDeleted: false });
|
||||||
const userMap: Record<string, string[]> = {};
|
const usersIds = await Promise.all(
|
||||||
for (const user of users) {
|
users.map((user) =>
|
||||||
const partnerIds = await getMyPartnerIds({
|
getMyPartnerIds({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
repository: this.partnerRepository,
|
repository: this.partnerRepository,
|
||||||
timelineEnabled: true,
|
timelineEnabled: true,
|
||||||
});
|
}),
|
||||||
userMap[user.id] = [user.id, ...partnerIds];
|
),
|
||||||
}
|
);
|
||||||
|
|
||||||
const start = DateTime.utc().startOf('day').minus({ days: DAYS });
|
|
||||||
|
|
||||||
|
await this.databaseRepository.withLock(DatabaseLock.MemoryCreation, async () => {
|
||||||
const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE);
|
const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE);
|
||||||
|
const start = DateTime.utc().startOf('day').minus({ days: DAYS });
|
||||||
const lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state.lastOnThisDayDate) : start;
|
const lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state.lastOnThisDayDate) : start;
|
||||||
|
|
||||||
// generate a memory +/- X days from today
|
// generate a memory +/- X days from today
|
||||||
@ -38,33 +37,39 @@ export class MemoryService extends BaseService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAt = target.startOf('day').toISO();
|
try {
|
||||||
const hideAt = target.endOf('day').toISO();
|
await Promise.all(users.map((owner, i) => this.createOnThisDayMemories(owner.id, usersIds[i], target)));
|
||||||
|
} catch (error) {
|
||||||
for (const [userId, userIds] of Object.entries(userMap)) {
|
this.logger.error(`Failed to create memories for ${target.toISO()}`, error);
|
||||||
const memories = await this.assetRepository.getByDayOfYear(userIds, target);
|
|
||||||
|
|
||||||
for (const { year, assets } of memories) {
|
|
||||||
const data: OnThisDayData = { year };
|
|
||||||
await this.memoryRepository.create(
|
|
||||||
{
|
|
||||||
ownerId: userId,
|
|
||||||
type: MemoryType.ON_THIS_DAY,
|
|
||||||
data,
|
|
||||||
memoryAt: target.set({ year }).toISO(),
|
|
||||||
showAt,
|
|
||||||
hideAt,
|
|
||||||
},
|
|
||||||
new Set(assets.map(({ id }) => id)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
// update system metadata even when there is an error to minimize the chance of duplicates
|
||||||
|
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.MEMORIES_STATE, {
|
await this.systemMetadataRepository.set(SystemMetadataKey.MEMORIES_STATE, {
|
||||||
...state,
|
...state,
|
||||||
lastOnThisDayDate: target.toISO(),
|
lastOnThisDayDate: target.toISO(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createOnThisDayMemories(ownerId: string, userIds: string[], target: DateTime) {
|
||||||
|
const showAt = target.startOf('day').toISO();
|
||||||
|
const hideAt = target.endOf('day').toISO();
|
||||||
|
const memories = await this.assetRepository.getByDayOfYear([ownerId, ...userIds], target);
|
||||||
|
await Promise.all(
|
||||||
|
memories.map(({ year, assets }) =>
|
||||||
|
this.memoryRepository.create(
|
||||||
|
{
|
||||||
|
ownerId,
|
||||||
|
type: MemoryType.ON_THIS_DAY,
|
||||||
|
data: { year },
|
||||||
|
memoryAt: target.set({ year }).toISO()!,
|
||||||
|
showAt,
|
||||||
|
hideAt,
|
||||||
|
},
|
||||||
|
new Set(assets.map(({ id }) => id)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.MEMORIES_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
@OnJob({ name: JobName.MEMORIES_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||||
|
@ -15,6 +15,7 @@ describe(MemoryService.name, () => {
|
|||||||
database: db || defaultDatabase,
|
database: db || defaultDatabase,
|
||||||
repos: {
|
repos: {
|
||||||
asset: 'real',
|
asset: 'real',
|
||||||
|
database: 'real',
|
||||||
memory: 'real',
|
memory: 'real',
|
||||||
user: 'real',
|
user: 'real',
|
||||||
systemMetadata: 'real',
|
systemMetadata: 'real',
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
checked?: boolean | undefined;
|
|
||||||
disabled?: boolean;
|
|
||||||
labelClass?: string | undefined;
|
|
||||||
name?: string | undefined;
|
|
||||||
value?: string | undefined;
|
|
||||||
onchange?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
checked = $bindable(),
|
|
||||||
disabled = false,
|
|
||||||
labelClass = undefined,
|
|
||||||
name = undefined,
|
|
||||||
value = undefined,
|
|
||||||
onchange = () => {},
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
{name}
|
|
||||||
{id}
|
|
||||||
{value}
|
|
||||||
{disabled}
|
|
||||||
class="size-5 flex-shrink-0 focus-visible:ring"
|
|
||||||
bind:checked
|
|
||||||
{onchange}
|
|
||||||
/>
|
|
||||||
<label class={labelClass} for={id}>{label}</label>
|
|
||||||
</div>
|
|
@ -1,94 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
/**
|
|
||||||
* Unique identifier for the checkbox element, used to associate labels with the input element.
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Optional aria-describedby attribute to associate the checkbox with a description.
|
|
||||||
*/
|
|
||||||
ariaDescribedBy?: string | undefined;
|
|
||||||
checked?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
onToggle?: ((checked: boolean) => void) | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
id,
|
|
||||||
ariaDescribedBy = undefined,
|
|
||||||
checked = $bindable(false),
|
|
||||||
disabled = false,
|
|
||||||
onToggle = undefined,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const handleToggle = (event: Event) => onToggle?.((event.target as HTMLInputElement).checked);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<label class="relative inline-block h-[10px] w-[36px] flex-none">
|
|
||||||
<input
|
|
||||||
{id}
|
|
||||||
class="disabled::cursor-not-allowed h-0 w-0 opacity-0 peer"
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked
|
|
||||||
onclick={handleToggle}
|
|
||||||
{disabled}
|
|
||||||
aria-describedby={ariaDescribedBy}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if disabled}
|
|
||||||
<span
|
|
||||||
class="slider slider-disabled cursor-not-allowed border border-transparent before:border before:border-transparent"
|
|
||||||
></span>
|
|
||||||
{:else}
|
|
||||||
<span
|
|
||||||
class="slider slider-enabled cursor-pointer border-2 border-transparent before:border-2 before:border-transparent peer-focus-visible:outline before:peer-focus-visible:outline peer-focus-visible:dark:outline-gray-200 before:peer-focus-visible:dark:outline-gray-200 peer-focus-visible:outline-gray-600 before:peer-focus-visible:outline-gray-600 peer-focus-visible:dark:border-black before:peer-focus-visible:dark:border-black peer-focus-visible:border-white before:peer-focus-visible:border-white"
|
|
||||||
></span>
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.slider {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #ccc;
|
|
||||||
-webkit-transition: transform 0.4s;
|
|
||||||
transition: transform 0.4s;
|
|
||||||
border-radius: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider:before {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
left: -2px;
|
|
||||||
right: 0px;
|
|
||||||
bottom: -6px;
|
|
||||||
background-color: gray;
|
|
||||||
-webkit-transition: transform 0.4s;
|
|
||||||
transition: transform 0.4s;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked + .slider:before {
|
|
||||||
-webkit-transform: translateX(18px);
|
|
||||||
-ms-transform: translateX(18px);
|
|
||||||
transform: translateX(18px);
|
|
||||||
background-color: #4250af;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked + .slider-disabled {
|
|
||||||
background-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked + .slider-enabled {
|
|
||||||
background-color: #adcbfa;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -8,6 +8,7 @@
|
|||||||
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 ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
|
import ChangeDescription from '$lib/components/photos-page/actions/change-description-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 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';
|
||||||
@ -323,6 +324,7 @@
|
|||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleDeleteOrArchiveAssets} />
|
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleDeleteOrArchiveAssets} />
|
||||||
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
|
import AssetUpdateDecriptionConfirmModal from '$lib/modals/AssetUpdateDecriptionConfirmModal.svelte';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { updateAssets } from '@immich/sdk';
|
||||||
|
import { mdiText } 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';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menuItem?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { menuItem = false }: Props = $props();
|
||||||
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
|
const handleUpdateDescription = async () => {
|
||||||
|
const description = await modalManager.show(AssetUpdateDecriptionConfirmModal, {});
|
||||||
|
if (description) {
|
||||||
|
const ids = getSelectedAssets(getOwnedAssets(), $user);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateAssets({ assetBulkUpdateDto: { ids, description } });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_change_description'));
|
||||||
|
}
|
||||||
|
clearSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if menuItem}
|
||||||
|
<MenuOption text={$t('change_description')} icon={mdiText} onClick={() => handleUpdateDescription()} />
|
||||||
|
{/if}
|
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
|
||||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
|
import { Checkbox, Label } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -38,8 +38,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<p><b>{$t('cannot_undo_this_action')}</b></p>
|
<p><b>{$t('cannot_undo_this_action')}</b></p>
|
||||||
|
|
||||||
<div class="pt-4 flex justify-center items-center">
|
<div class="pt-4 flex justify-center items-center gap-2">
|
||||||
<Checkbox id="confirm-deletion-input" label={$t('do_not_show_again')} bind:checked />
|
<Checkbox id="confirm-deletion-input" bind:checked color="secondary" />
|
||||||
|
<Label label={$t('do_not_show_again')} for="confirm-deletion-input" />
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Theme } from '$lib/constants';
|
|
||||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
@ -12,13 +10,7 @@
|
|||||||
|
|
||||||
const { value, width, alt = $t('alt_text_qr_code') }: Props = $props();
|
const { value, width, alt = $t('alt_text_qr_code') }: Props = $props();
|
||||||
|
|
||||||
let promise = $derived(
|
let promise = $derived(QRCode.toDataURL(value, { margin: 0, width }));
|
||||||
QRCode.toDataURL(value, {
|
|
||||||
color: { dark: themeManager.value === Theme.DARK ? '#ffffffff' : '#000000ff', light: '#00000000' },
|
|
||||||
margin: 0,
|
|
||||||
width,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="width: {width}px; height: {width}px">
|
<div style="width: {width}px; height: {width}px">
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
export interface SearchDisplayFilters {
|
export interface SearchDisplayFilters {
|
||||||
isNotInAlbum?: boolean;
|
isNotInAlbum: boolean;
|
||||||
isArchive?: boolean;
|
isArchive: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite: boolean;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
import { Checkbox, Label } from '@immich/ui';
|
||||||
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -21,9 +22,18 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend class="immich-form-label">{$t('display_options').toUpperCase()}</legend>
|
<legend class="immich-form-label">{$t('display_options').toUpperCase()}</legend>
|
||||||
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
|
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
|
||||||
<Checkbox id="not-in-album-checkbox" label={$t('not_in_any_album')} bind:checked={filters.isNotInAlbum} />
|
<div class="flex items-center gap-2">
|
||||||
<Checkbox id="archive-checkbox" label={$t('archive')} bind:checked={filters.isArchive} />
|
<Checkbox id="not-in-album-checkbox" size="tiny" bind:checked={filters.isNotInAlbum} />
|
||||||
<Checkbox id="favorite-checkbox" label={$t('favorites')} bind:checked={filters.isFavorite} />
|
<Label label={$t('not_in_any_album')} for="not-in-album-checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox id="archive-checkbox" size="tiny" bind:checked={filters.isArchive} />
|
||||||
|
<Label label={$t('archive')} for="archive-checkbox" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox id="favorites-checkbox" size="tiny" bind:checked={filters.isFavorite} />
|
||||||
|
<Label label={$t('favorites')} for="favorites-checkbox" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
import { Checkbox, Label } from '@immich/ui';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string[];
|
value: string[];
|
||||||
@ -52,14 +52,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#each options as option (option.value)}
|
{#each options as option (option.value)}
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
size="tiny"
|
||||||
id="{option.value}-checkbox"
|
id="{option.value}-checkbox"
|
||||||
label={option.text}
|
|
||||||
checked={value.includes(option.value)}
|
checked={value.includes(option.value)}
|
||||||
{disabled}
|
{disabled}
|
||||||
labelClass="text-gray-500 dark:text-gray-300"
|
onCheckedChange={() => handleCheckboxChange(option.value)}
|
||||||
onchange={() => handleCheckboxChange(option.value)}
|
|
||||||
/>
|
/>
|
||||||
|
<Label label={option.text} for="{option.value}-checkbox">
|
||||||
|
{option.text}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { generateId } from '$lib/utils/generate-id';
|
||||||
|
import { Switch } from '@immich/ui';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import Slider from '$lib/components/elements/slider.svelte';
|
|
||||||
import { generateId } from '$lib/utils/generate-id';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@ -54,5 +54,5 @@
|
|||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Slider id={sliderId} bind:checked {disabled} {onToggle} ariaDescribedBy={subtitleId} />
|
<Switch id={sliderId} bind:checked {disabled} onCheckedChange={onToggle} aria-describedby={subtitleId} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,6 +25,13 @@
|
|||||||
|
|
||||||
let { onClose = () => {} }: Props = $props();
|
let { onClose = () => {} }: Props = $props();
|
||||||
|
|
||||||
|
// Temporary variables to hold the settings - marked as reactive with $state() but initialized with store values
|
||||||
|
let tempSlideshowDelay = $state($slideshowDelay);
|
||||||
|
let tempShowProgressBar = $state($showProgressBar);
|
||||||
|
let tempSlideshowNavigation = $state($slideshowNavigation);
|
||||||
|
let tempSlideshowLook = $state($slideshowLook);
|
||||||
|
let tempSlideshowTransition = $state($slideshowTransition);
|
||||||
|
|
||||||
const navigationOptions: Record<SlideshowNavigation, RenderedOption> = {
|
const navigationOptions: Record<SlideshowNavigation, RenderedOption> = {
|
||||||
[SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: $t('shuffle') },
|
[SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: $t('shuffle') },
|
||||||
[SlideshowNavigation.AscendingOrder]: { icon: mdiArrowUpThin, title: $t('backward') },
|
[SlideshowNavigation.AscendingOrder]: { icon: mdiArrowUpThin, title: $t('backward') },
|
||||||
@ -47,6 +54,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyChanges = () => {
|
||||||
|
$slideshowDelay = tempSlideshowDelay;
|
||||||
|
$showProgressBar = tempShowProgressBar;
|
||||||
|
$slideshowNavigation = tempSlideshowNavigation;
|
||||||
|
$slideshowLook = tempSlideshowLook;
|
||||||
|
$slideshowTransition = tempSlideshowTransition;
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title={$t('slideshow_settings')} onClose={() => onClose()}>
|
<FullScreenModal title={$t('slideshow_settings')} onClose={() => onClose()}>
|
||||||
@ -54,31 +70,32 @@
|
|||||||
<SettingDropdown
|
<SettingDropdown
|
||||||
title={$t('direction')}
|
title={$t('direction')}
|
||||||
options={Object.values(navigationOptions)}
|
options={Object.values(navigationOptions)}
|
||||||
selectedOption={navigationOptions[$slideshowNavigation]}
|
selectedOption={navigationOptions[tempSlideshowNavigation]}
|
||||||
onToggle={(option) => {
|
onToggle={(option) => {
|
||||||
$slideshowNavigation = handleToggle(option, navigationOptions) || $slideshowNavigation;
|
tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SettingDropdown
|
<SettingDropdown
|
||||||
title={$t('look')}
|
title={$t('look')}
|
||||||
options={Object.values(lookOptions)}
|
options={Object.values(lookOptions)}
|
||||||
selectedOption={lookOptions[$slideshowLook]}
|
selectedOption={lookOptions[tempSlideshowLook]}
|
||||||
onToggle={(option) => {
|
onToggle={(option) => {
|
||||||
$slideshowLook = handleToggle(option, lookOptions) || $slideshowLook;
|
tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SettingSwitch title={$t('show_progress_bar')} bind:checked={$showProgressBar} />
|
<SettingSwitch title={$t('show_progress_bar')} bind:checked={tempShowProgressBar} />
|
||||||
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={$slideshowTransition} />
|
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={tempSlideshowTransition} />
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label={$t('duration')}
|
label={$t('duration')}
|
||||||
description={$t('admin.slideshow_duration_description')}
|
description={$t('admin.slideshow_duration_description')}
|
||||||
min={1}
|
min={1}
|
||||||
bind:value={$slideshowDelay}
|
bind:value={tempSlideshowDelay}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#snippet stickyBottom()}
|
{#snippet stickyBottom()}
|
||||||
<Button fullWidth shape="round" color="primary" onclick={() => onClose()}>{$t('done')}</Button>
|
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
||||||
|
<Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
|
import { dateFormats } from '$lib/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
keys: ApiKeyResponseDto[];
|
keys: ApiKeyResponseDto[];
|
||||||
@ -25,12 +26,6 @@
|
|||||||
|
|
||||||
let { keys = $bindable() }: Props = $props();
|
let { keys = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const format: Intl.DateTimeFormatOptions = {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
};
|
|
||||||
|
|
||||||
async function refreshKeys() {
|
async function refreshKeys() {
|
||||||
keys = await getApiKeys();
|
keys = await getApiKeys();
|
||||||
}
|
}
|
||||||
@ -130,7 +125,7 @@
|
|||||||
>
|
>
|
||||||
<td class="w-1/3 text-ellipsis px-4 text-sm">{key.name}</td>
|
<td class="w-1/3 text-ellipsis px-4 text-sm">{key.name}</td>
|
||||||
<td class="w-1/3 text-ellipsis px-4 text-sm"
|
<td class="w-1/3 text-ellipsis px-4 text-sm"
|
||||||
>{new Date(key.createdAt).toLocaleDateString($locale, format)}
|
>{new Date(key.createdAt).toLocaleDateString($locale, dateFormats.settings)}
|
||||||
</td>
|
</td>
|
||||||
<td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/3">
|
<td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/3">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
import { dateFormats } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
@ -132,7 +134,9 @@
|
|||||||
{#if $user.isAdmin && serverPurchaseInfo?.activatedAt}
|
{#if $user.isAdmin && serverPurchaseInfo?.activatedAt}
|
||||||
<p class="dark:text-white text-sm mt-1 col-start-2">
|
<p class="dark:text-white text-sm mt-1 col-start-2">
|
||||||
{$t('purchase_activated_time', {
|
{$t('purchase_activated_time', {
|
||||||
values: { date: new Date(serverPurchaseInfo.activatedAt) },
|
values: {
|
||||||
|
date: new Date(serverPurchaseInfo.activatedAt).toLocaleString($locale, dateFormats.settings),
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
@ -161,7 +165,9 @@
|
|||||||
{#if $user.license?.activatedAt}
|
{#if $user.license?.activatedAt}
|
||||||
<p class="dark:text-white text-sm mt-1 col-start-2">
|
<p class="dark:text-white text-sm mt-1 col-start-2">
|
||||||
{$t('purchase_activated_time', {
|
{$t('purchase_activated_time', {
|
||||||
values: { date: new Date($user.license?.activatedAt) },
|
values: {
|
||||||
|
date: new Date($user.license?.activatedAt).toLocaleString($locale, dateFormats.settings),
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -72,6 +72,11 @@ export const dateFormats = {
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
},
|
},
|
||||||
|
settings: <Intl.DateTimeFormatOptions>{
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum QueryParameter {
|
export enum QueryParameter {
|
||||||
|
29
web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte
Normal file
29
web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||||
|
import { Input } from '@immich/ui';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: (description?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { onClose }: Props = $props();
|
||||||
|
|
||||||
|
let description = $state('');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
confirmColor="primary"
|
||||||
|
title={$t('edit_description')}
|
||||||
|
prompt={$t('edit_description_prompt')}
|
||||||
|
onClose={(confirmed) => (confirmed ? onClose(description) : onClose())}
|
||||||
|
>
|
||||||
|
{#snippet promptSnippet()}
|
||||||
|
<div class="flex flex-col text-start gap-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label for="description">{$t('description')}</label>
|
||||||
|
<Input class="immich-form-input" id="description" bind:value={description} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</ConfirmModal>
|
@ -30,7 +30,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {title} onClose={() => onClose(false)} {size} class="bg-light text-dark">
|
<Modal {title} onClose={() => onClose(false)} {size}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#if promptSnippet}{@render promptSnippet()}{:else}
|
{#if promptSnippet}{@render promptSnippet()}{:else}
|
||||||
<p>{prompt}</p>
|
<p>{prompt}</p>
|
||||||
|
@ -12,13 +12,7 @@
|
|||||||
const { onClose, newPassword }: Props = $props();
|
const { onClose, newPassword }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal title={$t('password_reset_success')} icon={mdiCheck} onClose={() => onClose()} size="small">
|
||||||
title={$t('password_reset_success')}
|
|
||||||
icon={mdiCheck}
|
|
||||||
onClose={() => onClose()}
|
|
||||||
size="small"
|
|
||||||
class="bg-light text-dark"
|
|
||||||
>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<Text>{$t('admin.user_password_has_been_reset')}</Text>
|
<Text>{$t('admin.user_password_has_been_reset')}</Text>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
try {
|
try {
|
||||||
const updatedPerson = await updatePerson({
|
const updatedPerson = await updatePerson({
|
||||||
id: person.id,
|
id: person.id,
|
||||||
personUpdateDto: { birthDate: birthDate.length > 0 ? birthDate : null },
|
personUpdateDto: { birthDate },
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationController.show({ message: $t('date_of_birth_saved'), type: NotificationType.Info });
|
notificationController.show({ message: $t('date_of_birth_saved'), type: NotificationType.Info });
|
||||||
@ -53,6 +53,13 @@
|
|||||||
bind:value={birthDate}
|
bind:value={birthDate}
|
||||||
max={todayFormatted}
|
max={todayFormatted}
|
||||||
/>
|
/>
|
||||||
|
{#if person.birthDate}
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button shape="round" color="secondary" size="small" onclick={() => (birthDate = '')}>
|
||||||
|
{$t('clear')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@ -62,8 +69,8 @@
|
|||||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>
|
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>
|
||||||
{$t('cancel')}
|
{$t('cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" shape="round" color="primary" fullWidth>
|
<Button type="submit" shape="round" color="primary" fullWidth form="set-birth-date-form">
|
||||||
{$t('set')}
|
{$t('save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
@ -84,8 +84,8 @@
|
|||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
isArchive: searchQuery.visibility === AssetVisibility.Archive,
|
isArchive: searchQuery.visibility === AssetVisibility.Archive,
|
||||||
isFavorite: searchQuery.isFavorite,
|
isFavorite: searchQuery.isFavorite ?? false,
|
||||||
isNotInAlbum: 'isNotInAlbum' in searchQuery ? searchQuery.isNotInAlbum : undefined,
|
isNotInAlbum: 'isNotInAlbum' in searchQuery ? (searchQuery.isNotInAlbum ?? false) : false,
|
||||||
},
|
},
|
||||||
mediaType:
|
mediaType:
|
||||||
searchQuery.type === AssetTypeEnum.Image
|
searchQuery.type === AssetTypeEnum.Image
|
||||||
@ -105,7 +105,11 @@
|
|||||||
location: {},
|
location: {},
|
||||||
camera: {},
|
camera: {},
|
||||||
date: {},
|
date: {},
|
||||||
display: {},
|
display: {
|
||||||
|
isArchive: false,
|
||||||
|
isFavorite: false,
|
||||||
|
isNotInAlbum: false,
|
||||||
|
},
|
||||||
mediaType: MediaType.All,
|
mediaType: MediaType.All,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
};
|
};
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={$t('create_new_user')} {onClose} size="small" class="text-dark bg-light">
|
<Modal title={$t('create_new_user')} {onClose} size="small">
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form">
|
<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form">
|
||||||
{#if error}
|
{#if error}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
|
||||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||||
import { serverConfig } from '$lib/stores/server-config.store';
|
import { serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { deleteUserAdmin, type UserAdminResponseDto, type UserResponseDto } from '@immich/sdk';
|
import { deleteUserAdmin, type UserAdminResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||||
|
import { Checkbox, Label } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -66,16 +66,14 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-center m-4 gap-2">
|
<div class="flex justify-center items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="queue-user-deletion-checkbox"
|
id="queue-user-deletion-checkbox"
|
||||||
label={$t('admin.user_delete_immediately_checkbox')}
|
color="secondary"
|
||||||
labelClass="text-sm dark:text-immich-dark-fg"
|
|
||||||
bind:checked={forceDelete}
|
bind:checked={forceDelete}
|
||||||
onchange={() => {
|
onCheckedChange={() => (deleteButtonDisabled = forceDelete)}
|
||||||
deleteButtonDisabled = forceDelete;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<Label label={$t('admin.user_delete_immediately_checkbox')} for="queue-user-deletion-checkbox" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if forceDelete}
|
{#if forceDelete}
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={$t('edit_user')} size="small" icon={mdiAccountEditOutline} {onClose} class="text-dark bg-light">
|
<Modal title={$t('edit_user')} size="small" icon={mdiAccountEditOutline} {onClose}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<form onsubmit={onSubmit} autocomplete="off" id="edit-user-form">
|
<form onsubmit={onSubmit} autocomplete="off" id="edit-user-form">
|
||||||
<div class="mb-4 flex flex-col gap-2">
|
<div class="mb-4 flex flex-col gap-2">
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={$t('restore_user')} {onClose} icon={mdiDeleteRestore} size="small" class="bg-light text-dark">
|
<Modal title={$t('restore_user')} {onClose} icon={mdiDeleteRestore} size="small">
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>
|
<p>
|
||||||
<FormatMessage key="admin.user_restore_description" values={{ user: user.name }}>
|
<FormatMessage key="admin.user_restore_description" values={{ user: user.name }}>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
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 ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
|
import ChangeDescription from '$lib/components/photos-page/actions/change-description-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 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';
|
||||||
@ -478,6 +479,7 @@
|
|||||||
<DownloadAction menuItem filename="{album.albumName}.zip" />
|
<DownloadAction menuItem filename="{album.albumName}.zip" />
|
||||||
{#if assetInteraction.isAllUserOwned}
|
{#if assetInteraction.isAllUserOwned}
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
{#if assetInteraction.selectedAssets.length === 1}
|
{#if assetInteraction.selectedAssets.length === 1}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
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 ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
|
import ChangeDescription from '$lib/components/photos-page/actions/change-description-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 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';
|
||||||
@ -59,6 +60,7 @@
|
|||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
menuItem
|
menuItem
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
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 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 ChangeDescription from '$lib/components/photos-page/actions/change-description-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 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';
|
||||||
@ -115,6 +116,7 @@
|
|||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />
|
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />
|
||||||
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
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 ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
|
import ChangeDescription from '$lib/components/photos-page/actions/change-description-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 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';
|
||||||
@ -328,6 +329,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
person = updatedPerson;
|
||||||
people = people.map((person: PersonResponseDto) => {
|
people = people.map((person: PersonResponseDto) => {
|
||||||
if (person.id === updatedPerson.id) {
|
if (person.id === updatedPerson.id) {
|
||||||
return updatedPerson;
|
return updatedPerson;
|
||||||
@ -514,6 +516,7 @@
|
|||||||
onClick={handleReassignAssets}
|
onClick={handleReassignAssets}
|
||||||
/>
|
/>
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
menuItem
|
menuItem
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
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 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 ChangeDescription from '$lib/components/photos-page/actions/change-description-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 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';
|
||||||
@ -142,6 +143,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
<ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
|
<ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||||
{#if $preferences.tags.enabled}
|
{#if $preferences.tags.enabled}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
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 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 ChangeDescription from '$lib/components/photos-page/actions/change-description-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 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';
|
||||||
@ -249,57 +250,6 @@
|
|||||||
|
|
||||||
<svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onEscape }} bind:scrollY />
|
<svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onEscape }} bind:scrollY />
|
||||||
|
|
||||||
<section>
|
|
||||||
{#if assetInteraction.selectionActive}
|
|
||||||
<div class="fixed top-0 start-0 w-full">
|
|
||||||
<AssetSelectControlBar
|
|
||||||
assets={assetInteraction.selectedAssets}
|
|
||||||
clearSelect={() => cancelMultiselect(assetInteraction)}
|
|
||||||
>
|
|
||||||
<CreateSharedLink />
|
|
||||||
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
|
|
||||||
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
|
||||||
<AddToAlbum {onAddToAlbum} />
|
|
||||||
<AddToAlbum shared {onAddToAlbum} />
|
|
||||||
</ButtonContextMenu>
|
|
||||||
<FavoriteAction
|
|
||||||
removeFavorite={assetInteraction.isAllFavorite}
|
|
||||||
onFavorite={(ids, isFavorite) => {
|
|
||||||
for (const id of ids) {
|
|
||||||
const asset = searchResultAssets.find((asset) => asset.id === id);
|
|
||||||
if (asset) {
|
|
||||||
asset.isFavorite = isFavorite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
|
||||||
<DownloadAction menuItem />
|
|
||||||
<ChangeDate menuItem />
|
|
||||||
<ChangeLocation menuItem />
|
|
||||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
|
|
||||||
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
|
||||||
<TagAction menuItem />
|
|
||||||
{/if}
|
|
||||||
<DeleteAssets menuItem {onAssetDelete} />
|
|
||||||
<hr />
|
|
||||||
<AssetJobActions />
|
|
||||||
</ButtonContextMenu>
|
|
||||||
</AssetSelectControlBar>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="fixed top-0 start-0 w-full">
|
|
||||||
<ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
|
||||||
<div class="absolute bg-light"></div>
|
|
||||||
<div class="w-full flex-1 ps-4">
|
|
||||||
<SearchBar grayTheme={false} value={terms?.query ?? ''} searchQuery={terms} />
|
|
||||||
</div>
|
|
||||||
</ControlAppBar>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if terms}
|
{#if terms}
|
||||||
<section
|
<section
|
||||||
id="search-chips"
|
id="search-chips"
|
||||||
@ -380,4 +330,56 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
{#if assetInteraction.selectionActive}
|
||||||
|
<div class="fixed top-0 start-0 w-full">
|
||||||
|
<AssetSelectControlBar
|
||||||
|
assets={assetInteraction.selectedAssets}
|
||||||
|
clearSelect={() => cancelMultiselect(assetInteraction)}
|
||||||
|
>
|
||||||
|
<CreateSharedLink />
|
||||||
|
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
|
||||||
|
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||||
|
<AddToAlbum {onAddToAlbum} />
|
||||||
|
<AddToAlbum shared {onAddToAlbum} />
|
||||||
|
</ButtonContextMenu>
|
||||||
|
<FavoriteAction
|
||||||
|
removeFavorite={assetInteraction.isAllFavorite}
|
||||||
|
onFavorite={(ids, isFavorite) => {
|
||||||
|
for (const id of ids) {
|
||||||
|
const asset = searchResultAssets.find((asset) => asset.id === id);
|
||||||
|
if (asset) {
|
||||||
|
asset.isFavorite = isFavorite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
|
<DownloadAction menuItem />
|
||||||
|
<ChangeDate menuItem />
|
||||||
|
<ChangeDescription menuItem />
|
||||||
|
<ChangeLocation menuItem />
|
||||||
|
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
|
||||||
|
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
||||||
|
<TagAction menuItem />
|
||||||
|
{/if}
|
||||||
|
<DeleteAssets menuItem {onAssetDelete} />
|
||||||
|
<hr />
|
||||||
|
<AssetJobActions />
|
||||||
|
</ButtonContextMenu>
|
||||||
|
</AssetSelectControlBar>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="fixed top-0 start-0 w-full">
|
||||||
|
<ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
||||||
|
<div class="absolute bg-light"></div>
|
||||||
|
<div class="w-full flex-1 ps-4">
|
||||||
|
<SearchBar grayTheme={false} value={terms?.query ?? ''} searchQuery={terms} />
|
||||||
|
</div>
|
||||||
|
</ControlAppBar>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user