Compare commits

...

9 Commits

Author SHA1 Message Date
Alex The Bot
81d51fbd7e Version v1.81.0 2023-10-03 20:48:23 +00:00
Alex
02f9b40d67 fix(server): library control doesn't apply to new library from the third row (#4331) 2023-10-03 14:05:14 -05:00
Jason Rasmussen
260a600bbc chore(server): dev compose changes (#4316) 2023-10-03 13:06:08 -05:00
Jason Rasmussen
818005fcb5 fix(server): fallback to local timezone when rendering storage template (#4317) 2023-10-03 13:05:44 -05:00
Daniel Dietzler
e5f704cf3b fix asset upload permissions for shared links (#4325) 2023-10-03 12:36:51 -04:00
Jonathan Jogenfors
e2f1e38472 chore(server,web): bump node version to 20.8 (#4311)
* chore: bump node version to 20.8

* fix: remove node hash
2023-10-03 09:34:35 -05:00
Alex
b3c82d5ba2 fix(server): incorrect video creation date EXIF extraction (#4309)
* fix(server): incorrect video creation date EXIF extraction

* update dependency

* update dependency

* revert

* remove unused code
2023-10-03 08:51:40 -05:00
Jonathan Jogenfors
6d1868a6e0 feat: server containers use host timezone (#4313) 2023-10-02 20:50:27 -05:00
Daniel Dietzler
98db9331d8 fix(server): delete face thumbnails when merging people (#4310)
* new job for person deletion, including face thumbnail deletion

* fix tests, delete files directly instead queueing jobs
2023-10-02 21:15:11 -04:00
33 changed files with 244 additions and 196 deletions

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -11,8 +11,10 @@ services:
command: npm run start:debug immich
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- 3001:3001
- 9230:9230
@@ -25,25 +27,6 @@ services:
- database
- typesense
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning-dev:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
ports:
- 3003:3003
volumes:
- ../machine-learning:/usr/src/app
- model-cache:/cache
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
restart: unless-stopped
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
@@ -57,8 +40,10 @@ services:
command: npm run start:debug microservices
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
@@ -94,6 +79,25 @@ services:
depends_on:
- immich-server
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning-dev:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
ports:
- 3003:3003
volumes:
- ../machine-learning:/usr/src/app
- model-cache:/cache
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
restart: unless-stopped
typesense:
container_name: immich_typesense
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
@@ -103,7 +107,7 @@ services:
# remove this to get debug messages
- GLOG_minloglevel=1
volumes:
- tsdata:/data
- ${UPLOAD_LOCATION}/typesense:/data
redis:
container_name: immich_redis
@@ -119,7 +123,7 @@ services:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
- ${UPLOAD_LOCATION}/postgres:/data
ports:
- 5432:5432
@@ -141,6 +145,4 @@ services:
restart: unless-stopped
volumes:
pgdata:
model-cache:
tsdata:

View File

@@ -10,6 +10,8 @@ services:
command: ["./start-server.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
@@ -29,7 +31,7 @@ services:
env_file:
- .env
restart: always
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
@@ -42,6 +44,8 @@ services:
command: ["./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:

View File

@@ -4,9 +4,11 @@ services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
command: ["start.sh", "immich"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
@@ -21,9 +23,11 @@ services:
# extends:
# file: hwaccel.yml
# service: hwaccel
command: [ "start.sh", "microservices" ]
command: ["start.sh", "microservices"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.80.0"
version = "1.81.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 104,
"android.injected.version.name" => "1.80.0",
"android.injected.version.code" => 105,
"android.injected.version.name" => "1.81.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.80.0"
version_number: "1.81.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.80.0
- API version: 1.81.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.80.0+104
version: 1.81.0+105
isar_version: &isar_version 3.1.0+1
environment:

View File

@@ -1,4 +1,4 @@
FROM node:18-bookworm@sha256:c85dc4392f44f5de1d0d72dd20a088a542734445f99bed7aa8ac895c706d370d as builder
FROM node:20.8-bookworm as builder
WORKDIR /usr/src/app
@@ -29,7 +29,7 @@ FROM builder as prod
RUN npm run build
RUN npm prune --omit=dev --omit=optional
FROM node:18-bookworm-slim@sha256:a0cca98f2896135d4c0386922211c1f90f98f27a58b8f2c07850d0fbe1c2104e
FROM node:20.8-bookworm
ENV NODE_ENV=production

View File

@@ -5099,7 +5099,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.80.0",
"version": "1.81.0",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.80.0",
"version": "1.81.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.80.0",
"version": "1.81.0",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.80.0",
"version": "1.81.0",
"description": "",
"author": "",
"private": true,

View File

@@ -10,6 +10,7 @@ export enum Permission {
ASSET_SHARE = 'asset.share',
ASSET_VIEW = 'asset.view',
ASSET_DOWNLOAD = 'asset.download',
ASSET_UPLOAD = 'asset.upload',
// ALBUM_CREATE = 'album.create',
ALBUM_READ = 'album.read',
@@ -26,7 +27,6 @@ export enum Permission {
LIBRARY_CREATE = 'library.create',
LIBRARY_READ = 'library.read',
LIBRARY_WRITE = 'library.write',
LIBRARY_UPDATE = 'library.update',
LIBRARY_DELETE = 'library.delete',
LIBRARY_DOWNLOAD = 'library.download',
@@ -96,6 +96,9 @@ export class AccessCore {
case Permission.ASSET_DOWNLOAD:
return !!authUser.isAllowDownload && (await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id));
case Permission.ASSET_UPLOAD:
return authUser.isAllowUpload;
case Permission.ASSET_SHARE:
// TODO: fix this to not use authUser.id for shared link access control
return this.repository.asset.hasOwnerAccess(authUser.id, id);
@@ -166,6 +169,9 @@ export class AccessCore {
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
);
case Permission.ASSET_UPLOAD:
return this.repository.library.hasOwnerAccess(authUser.id, id);
case Permission.ALBUM_REMOVE_ASSET:
return this.repository.album.hasOwnerAccess(authUser.id, id);
@@ -184,9 +190,6 @@ export class AccessCore {
(await this.repository.library.hasPartnerAccess(authUser.id, id))
);
case Permission.LIBRARY_WRITE:
return this.repository.library.hasOwnerAccess(authUser.id, id);
case Permission.LIBRARY_UPDATE:
return this.repository.library.hasOwnerAccess(authUser.id, id);

View File

@@ -56,9 +56,10 @@ export enum JobName {
CLASSIFY_IMAGE = 'classify-image',
// facial recognition
PERSON_CLEANUP = 'person-cleanup',
PERSON_DELETE = 'person-delete',
QUEUE_RECOGNIZE_FACES = 'queue-recognize-faces',
RECOGNIZE_FACES = 'recognize-faces',
PERSON_CLEANUP = 'person-cleanup',
// library managment
LIBRARY_SCAN = 'library-refresh',
@@ -103,6 +104,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK,
[JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
[JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
[JobName.PERSON_DELETE]: QueueName.BACKGROUND_TASK,
// conversion
[JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,

View File

@@ -68,6 +68,7 @@ export type JobItem =
| { name: JobName.QUEUE_RECOGNIZE_FACES; data: IBaseJob }
| { name: JobName.RECOGNIZE_FACES; data: IEntityJob }
| { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob }
| { name: JobName.PERSON_DELETE; data: IEntityJob }
// Clip Embedding
| { name: JobName.QUEUE_ENCODE_CLIP; data: IBaseJob }

View File

@@ -311,7 +311,19 @@ export class MetadataService {
assetId: asset.id,
bitsPerSample: this.getBitsPerSample(tags),
colorspace: tags.ColorSpace ?? null,
dateTimeOriginal: exifDate(firstDateTime(tags as Tags)) ?? asset.fileCreatedAt,
dateTimeOriginal:
exifDate(
firstDateTime(tags as Tags, [
'SubSecDateTimeOriginal',
'DateTimeOriginal',
'SubSecCreateDate',
'CreationDate',
'CreateDate',
'SubSecMediaCreateDate',
'MediaCreateDate',
'DateTimeCreated',
]),
) ?? asset.fileCreatedAt,
exifImageHeight: validate(tags.ImageHeight),
exifImageWidth: validate(tags.ImageWidth),
exposureTime: tags.ExposureTime ?? null,

View File

@@ -373,11 +373,7 @@ describe(PersonService.name, () => {
await sut.handlePersonCleanup();
expect(personMock.delete).toHaveBeenCalledWith(personStub.noName);
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES,
data: { files: ['/path/to/thumbnail.jpg'] },
});
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.PERSON_DELETE, data: { id: personStub.noName.id } });
});
});
@@ -409,7 +405,7 @@ describe(PersonService.name, () => {
items: [assetStub.image],
hasNextPage: false,
});
personMock.deleteAll.mockResolvedValue(5);
personMock.getAll.mockResolvedValue([personStub.withName]);
searchMock.deleteAllFaces.mockResolvedValue(100);
await sut.handleQueueRecognizeFaces({ force: true });
@@ -419,6 +415,10 @@ describe(PersonService.name, () => {
name: JobName.RECOGNIZE_FACES,
data: { id: assetStub.image.id },
});
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.PERSON_DELETE,
data: { id: personStub.withName.id },
});
});
});
@@ -650,7 +650,10 @@ describe(PersonService.name, () => {
oldPersonId: personStub.mergePerson.id,
});
expect(personMock.delete).toHaveBeenCalledWith(personStub.mergePerson);
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.PERSON_DELETE,
data: { id: personStub.mergePerson.id },
});
expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
});

View File

@@ -139,16 +139,27 @@ export class PersonService {
return results;
}
async handlePersonDelete({ id }: IEntityJob) {
const person = await this.repository.getById(id);
if (!person) {
return false;
}
try {
await this.repository.delete(person);
await this.storageRepository.unlink(person.thumbnailPath);
} catch (error: Error | any) {
this.logger.error(`Unable to delete person: ${error}`, error?.stack);
}
return true;
}
async handlePersonCleanup() {
const people = await this.repository.getAllWithoutFaces();
for (const person of people) {
this.logger.debug(`Person ${person.name || person.id} no longer has any faces, deleting.`);
try {
await this.repository.delete(person);
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [person.thumbnailPath] } });
} catch (error: Error | any) {
this.logger.error(`Unable to delete person: ${error}`, error?.stack);
}
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: person.id } });
}
return true;
@@ -167,7 +178,10 @@ export class PersonService {
});
if (force) {
const people = await this.repository.deleteAll();
const people = await this.repository.getAll();
for (const person of people) {
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: person.id } });
}
const faces = await this.searchRepository.deleteAllFaces();
this.logger.debug(`Deleted ${people} people and ${faces} faces`);
}
@@ -363,7 +377,7 @@ export class PersonService {
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId: mergeId } });
}
await this.repository.reassignFaces(mergeData);
await this.repository.delete(mergePerson);
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: mergePerson.id } });
this.logger.log(`Merged ${mergeName} into ${primaryName}`);
results.push({ id: mergeId, success: true });

View File

@@ -235,7 +235,9 @@ export class StorageTemplateService {
filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
};
const dt = luxon.DateTime.fromJSDate(asset.fileCreatedAt, { zone: asset.exifInfo?.timeZone || undefined });
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const zone = asset.exifInfo?.timeZone || systemTimeZone;
const dt = luxon.DateTime.fromJSDate(asset.fileCreatedAt, { zone });
const dateTokens = [
...supportedYearTokens,

View File

@@ -91,7 +91,7 @@ export class AssetService {
try {
const libraryId = await this.getLibraryId(authUser, dto.libraryId);
await this.access.requirePermission(authUser, Permission.LIBRARY_WRITE, libraryId);
await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId);
if (livePhotoFile) {
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile);
@@ -163,7 +163,7 @@ export class AssetService {
try {
const libraryId = await this.getLibraryId(authUser, dto.libraryId);
await this.access.requirePermission(authUser, Permission.LIBRARY_WRITE, libraryId);
await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId);
const asset = await this.assetCore.create(authUser, { ...dto, libraryId }, assetFile, undefined, dto.sidecarPath);
return { id: asset.id, duplicate: false };
} catch (error: QueryFailedError | Error | any) {

View File

@@ -74,6 +74,7 @@ export class AppService {
[JobName.RECOGNIZE_FACES]: (data) => this.personService.handleRecognizeFaces(data),
[JobName.GENERATE_PERSON_THUMBNAIL]: (data) => this.personService.handleGeneratePersonThumbnail(data),
[JobName.PERSON_CLEANUP]: () => this.personService.handlePersonCleanup(),
[JobName.PERSON_DELETE]: (data) => this.personService.handlePersonDelete(data),
[JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
[JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(),

View File

@@ -1,5 +1,5 @@
# Our Node base image
FROM node:18.16.0-alpine3.18@sha256:9036ddb8252ba7089c2c83eb2b0dcaf74ff1069e8ddf86fe2bd6dc5fecc9492d as base
FROM node:20.8-alpine3.18 as base
WORKDIR /usr/src/app
EXPOSE 3000

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -43,7 +43,8 @@
let dropdownOpen: boolean[] = [];
let showContextMenu = false;
let contextMenuPosition = { x: 0, y: 0 };
let libraryType: LibraryType;
let selectedLibraryIndex = 0;
let selectedLibrary: LibraryResponseDto | null = null;
onMount(() => {
readLibraryList();
@@ -61,10 +62,12 @@
}
};
const showMenu = (event: MouseEvent, type: LibraryType) => {
const showMenu = (event: MouseEvent, library: LibraryResponseDto, index: number) => {
contextMenuPosition = getContextMenuPosition(event);
showContextMenu = !showContextMenu;
libraryType = type;
selectedLibraryIndex = index;
selectedLibrary = library;
};
const onMenuExit = () => {
@@ -216,54 +219,63 @@
}
};
const onRenameClicked = (index: number) => {
const onRenameClicked = () => {
closeAll();
renameLibrary = index;
updateLibraryIndex = index;
renameLibrary = selectedLibraryIndex;
updateLibraryIndex = selectedLibraryIndex;
};
const onEditImportPathClicked = (index: number) => {
const onEditImportPathClicked = () => {
closeAll();
editImportPaths = index;
updateLibraryIndex = index;
editImportPaths = selectedLibraryIndex;
updateLibraryIndex = selectedLibraryIndex;
};
const onScanNewLibraryClicked = (libraryId: string) => {
const onScanNewLibraryClicked = () => {
closeAll();
handleScan(libraryId);
if (selectedLibrary) {
handleScan(selectedLibrary.id);
}
};
const onScanSettingClicked = (index: number) => {
const onScanSettingClicked = () => {
closeAll();
editScanSettings = index;
updateLibraryIndex = index;
editScanSettings = selectedLibraryIndex;
updateLibraryIndex = selectedLibraryIndex;
};
const onScanAllLibraryFilesClicked = (libraryId: string) => {
const onScanAllLibraryFilesClicked = () => {
closeAll();
handleScanChanges(libraryId);
if (selectedLibrary) {
handleScanChanges(selectedLibrary.id);
}
};
const onForceScanAllLibraryFilesClicked = (libraryId: string) => {
const onForceScanAllLibraryFilesClicked = () => {
closeAll();
handleForceScan(libraryId);
if (selectedLibrary) {
handleForceScan(selectedLibrary.id);
}
};
const onRemoveOfflineFilesClicked = (libraryId: string) => {
const onRemoveOfflineFilesClicked = () => {
closeAll();
handleRemoveOffline(libraryId);
if (selectedLibrary) {
handleRemoveOffline(selectedLibrary.id);
}
};
const onDeleteLibraryClicked = (index: number, library: LibraryResponseDto) => {
const onDeleteLibraryClicked = () => {
closeAll();
if (confirm(`Are you sure you want to delete ${library.name} library?`) == true) {
refreshStats(index);
if (totalCount[index] > 0) {
deleteAssetCount = totalCount[index];
confirmDeleteLibrary = library;
if (selectedLibrary && confirm(`Are you sure you want to delete ${selectedLibrary.name} library?`) == true) {
refreshStats(selectedLibraryIndex);
if (totalCount[selectedLibraryIndex] > 0) {
deleteAssetCount = totalCount[selectedLibraryIndex];
confirmDeleteLibrary = selectedLibrary;
} else {
deleteLibrary = library;
deleteLibrary = selectedLibrary;
handleDelete();
}
}
@@ -295,104 +307,92 @@
</tr>
</thead>
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each libraries as library, index}
{#key library.id}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
index % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
{#each libraries as library, index (library.id)}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
index % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="w-1/6 px-10 text-sm">
{#if library.type === LibraryType.External}
<Database size="40" title="External library (created on {library.createdAt})" />
{:else if library.type === LibraryType.Upload}
<Upload size="40" title="Upload library (created on {library.createdAt})" />
{/if}</td
>
<td class="w-1/6 px-10 text-sm">
{#if library.type === LibraryType.External}
<Database size="40" title="External library (created on {library.createdAt})" />
{:else if library.type === LibraryType.Upload}
<Upload size="40" title="Upload library (created on {library.createdAt})" />
{/if}</td
>
<td class="w-1/3 text-ellipsis px-4 text-sm">{library.name}</td>
{#if totalCount[index] == undefined}
<td colspan="2" class="flex w-1/3 items-center justify-center text-ellipsis px-4 text-sm">
<Pulse color="gray" size="40" unit="px" />
</td>
{:else}
<td class="w-1/6 text-ellipsis px-4 text-sm">
{totalCount[index]}
</td>
<td class="w-1/6 text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]} </td>
{/if}
<td class="w-1/6 text-ellipsis px-4 text-sm">
<button
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
on:click|stopPropagation|preventDefault={(e) => showMenu(e, library.type)}
>
<DotsVertical size="16" />
</button>
{#if showContextMenu}
<Portal target="body">
<ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
<MenuOption on:click={() => onRenameClicked(index)} text="Rename" />
{#if libraryType === LibraryType.External}
<MenuOption on:click={() => onEditImportPathClicked(index)} text="Edit Import Paths" />
<MenuOption on:click={() => onScanSettingClicked(index)} text="Scan Settings" />
<hr />
<MenuOption
on:click={() => onScanNewLibraryClicked(library.id)}
text="Scan New Library Files"
/>
<MenuOption
on:click={() => onScanAllLibraryFilesClicked(library.id)}
text="Re-scan All Library Files"
subtitle={'Only refreshes modified files'}
/>
<MenuOption
on:click={() => onForceScanAllLibraryFilesClicked(library.id)}
text="Force Re-scan All Library Files"
subtitle={'Refreshes every file'}
/>
<hr />
<MenuOption
on:click={() => onRemoveOfflineFilesClicked(library.id)}
text="Remove Offline Files"
/>
<MenuOption on:click={() => onDeleteLibraryClicked(index, library)}>
<p class="text-red-600">Delete library</p>
</MenuOption>
{/if}
</ContextMenu>
</Portal>
{/if}
<td class="w-1/3 text-ellipsis px-4 text-sm">{library.name}</td>
{#if totalCount[index] == undefined}
<td colspan="2" class="flex w-1/3 items-center justify-center text-ellipsis px-4 text-sm">
<Pulse color="gray" size="40" unit="px" />
</td>
</tr>
{#if renameLibrary === index}
<div transition:slide={{ duration: 250 }}>
<LibraryRenameForm {library} on:submit={handleUpdate} on:cancel={() => (renameLibrary = null)} />
</div>
{:else}
<td class="w-1/6 text-ellipsis px-4 text-sm">
{totalCount[index]}
</td>
<td class="w-1/6 text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]}</td>
{/if}
{#if editImportPaths === index}
<div transition:slide={{ duration: 250 }}>
<LibraryImportPathsForm
{library}
on:submit={handleUpdate}
on:cancel={() => (editImportPaths = null)}
/>
</div>
{/if}
{#if editScanSettings === index}
<div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
<LibraryScanSettingsForm
{library}
on:submit={handleUpdate}
on:cancel={() => (editScanSettings = null)}
/>
</div>
{/if}
{/key}
<td class="w-1/6 text-ellipsis px-4 text-sm">
<button
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
on:click|stopPropagation|preventDefault={(e) => showMenu(e, library, index)}
>
<DotsVertical size="16" />
</button>
{#if showContextMenu}
<Portal target="body">
<ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
<MenuOption on:click={() => onRenameClicked()} text={`Rename`} />
{#if selectedLibrary && selectedLibrary.type === LibraryType.External}
<MenuOption on:click={() => onEditImportPathClicked()} text="Edit Import Paths" />
<MenuOption on:click={() => onScanSettingClicked()} text="Scan Settings" />
<hr />
<MenuOption on:click={() => onScanNewLibraryClicked()} text="Scan New Library Files" />
<MenuOption
on:click={() => onScanAllLibraryFilesClicked()}
text="Re-scan All Library Files"
subtitle={'Only refreshes modified files'}
/>
<MenuOption
on:click={() => onForceScanAllLibraryFilesClicked()}
text="Force Re-scan All Library Files"
subtitle={'Refreshes every file'}
/>
<hr />
<MenuOption on:click={() => onRemoveOfflineFilesClicked()} text="Remove Offline Files" />
<MenuOption on:click={() => onDeleteLibraryClicked()}>
<p class="text-red-600">Delete library</p>
</MenuOption>
{/if}
</ContextMenu>
</Portal>
{/if}
</td>
</tr>
{#if renameLibrary === index}
<div transition:slide={{ duration: 250 }}>
<LibraryRenameForm {library} on:submit={handleUpdate} on:cancel={() => (renameLibrary = null)} />
</div>
{/if}
{#if editImportPaths === index}
<div transition:slide={{ duration: 250 }}>
<LibraryImportPathsForm {library} on:submit={handleUpdate} on:cancel={() => (editImportPaths = null)} />
</div>
{/if}
{#if editScanSettings === index}
<div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
<LibraryScanSettingsForm
{library}
on:submit={handleUpdate}
on:cancel={() => (editScanSettings = null)}
/>
</div>
{/if}
{/each}
</tbody>
</table>