Compare commits

..

25 Commits

Author SHA1 Message Date
Alex Tran
1cb5c617c8 wip: update 2024-12-28 23:09:32 -06:00
Alex Tran
c9e03003c0 wip: update 2024-12-28 23:09:24 -06:00
Alex Tran
677fbca7b5 merge main 2024-12-27 14:40:13 -06:00
mehring
34ce61d03a feat(web): create tag on the fly (#14726) 2024-12-27 17:29:57 +00:00
Lukas
0250a7a23a fix(web): Fix for failing to load pictures (#14943)
* attempt at fix for failing to load pictures

* comments

* remove unused files

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-12-27 16:16:07 +00:00
renovate[bot]
b91f39d1af chore(deps): update base-image to v20241224 (major) (#14905)
chore(deps): update base-image to v20241224

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-27 09:51:42 -06:00
Daniel Dietzler
139090715e fix: trusted proxies (#14888) 2024-12-27 09:51:07 -06:00
Sam Debruyn
05cea0fc69 chore(mobile): remove duplicate settingsservice (#14946)
remove duplicate settingsservice
2024-12-27 09:45:23 -06:00
Yaros
2255f3e966 feat(mobile): Modified draggable area of detail modal (#14953)
Modified draggable area of detail modal
2024-12-27 09:28:54 -06:00
Yaros
2be1cb7de2 fix(mobile): Fixed resolution format in Details (#14954)
Fixed resolution format on mobile
2024-12-27 09:20:07 -06:00
indam
227eb4b0a6 docs: Update Chinese README (#14926)
* Update Chinese README

* retrigger checks
2024-12-25 08:09:47 -05:00
bo0tzz
23461e98fb fix: clarify PR label validation message (#14925) 2024-12-25 08:07:52 -05:00
renovate[bot]
ef0070c3fd fix(deps): update machine-learning (#14891) 2024-12-23 20:04:55 -05:00
renovate[bot]
6b08e82cf7 fix(deps): update dependency @nestjs/swagger to v8 (#13881)
* fix(deps): update dependency @nestjs/swagger to v8

* chore: generate open api

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-12-23 21:03:34 +00:00
Ben
b88f98bf66 feat(web): Add "set as featured" option for an asset (#14879) 2024-12-23 12:26:53 -06:00
Jonathan Jogenfors
c3be74c450 fix(server): support import paths with special chars (#14856) 2024-12-22 16:22:16 -06:00
Jonathan Jogenfors
4bc2aa5451 feat(server): Handle sidecars in external libraries (#14800)
* handle sidecars in external libraries

* don't add separate source
2024-12-21 20:50:07 -06:00
Alex
6080e6e827 fix(web): infinite loop browser navigation crash admin settings page (#14850)
* fix(web): infinite loop browser navigation crash admin settings page

* pr feedback
2024-12-21 19:26:01 +00:00
Lukas
d5906c2efe feat(web): Adds toggle to disable sorting of faces (#14830)
* Allows for toggling of sorting in the merge face selector

* Adds toggle to the side panel for faces

* Improve layout and fix toggle

* chore: ui cleanup

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-12-21 16:53:37 +00:00
Matthew Momjian
b3821c50d7 chore(docs): FAQ cleanup (#14835)
* FAQ cleanup

* link fix

* copy paste error
2024-12-20 23:40:33 -06:00
Matthew Momjian
1cec3af98c chore(docs): fix broken link in FAQ (#14834)
broken link
2024-12-20 16:36:42 -06:00
Ben McCann
da70a1e457 chore: migrate to SvelteKit app state (#14807)
* chore: migrate to SvelteKit app state

* update package-lock.json
2024-12-20 17:18:22 -05:00
Alex
a14735846c chore(mobile): add timeout when reading video files (#14831) 2024-12-20 13:52:42 -06:00
Alex
f1577bc120 Merge branch 'main' of github.com:immich-app/immich into workflows 2024-09-15 23:12:14 -05:00
Alex Tran
b86e1c389e feat(web): workflows 2024-09-15 00:06:50 -05:00
58 changed files with 1281 additions and 957 deletions

View File

@@ -19,3 +19,4 @@ jobs:
use_regex: true use_regex: true
labels: "changelog:.*" labels: "changelog:.*"
add_comment: true add_comment: true
message: "Label error. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}. A maintainer will add the required label."

View File

@@ -69,7 +69,8 @@ However, Immich will delete original files that have been trashed when the trash
### Why do my file names appear as a random string in the file manager? ### Why do my file names appear as a random string in the file manager?
When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job. When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names.
To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation. It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation.
### Can I add my existing photo library? ### Can I add my existing photo library?
@@ -82,11 +83,20 @@ Template changes will only apply to _new_ assets. To retroactively apply the tem
### Why are only photos and not videos being uploaded to Immich? ### Why are only photos and not videos being uploaded to Immich?
This often happens when using a reverse proxy (such as Nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails. This often happens when using a reverse proxy in front of Immich.
Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to allow large requests.
Also, check the disk space of your reverse proxy.
In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
If you are using Cloudflare Tunnel, please know that they set a maxiumum filesize of 100 MB that cannot be changed.
At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
If you are having issues, we recommend switching to a different network deployment.
### Why are some photos stored in the file system with the wrong date? ### Why are some photos stored in the file system with the wrong date?
There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job. The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc., the job may not have run automatically the first time. There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job.
The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc.,
the job may not have run automatically the first time.
### How can I hide photos from the timeline? ### How can I hide photos from the timeline?
@@ -116,7 +126,8 @@ Also, there are additional jobs for person (face) thumbnails.
### Why do files from WhatsApp not appear with the correct date? ### Why do files from WhatsApp not appear with the correct date?
Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp, not the order of arrival on the device. [See #3527](https://github.com/immich-app/immich/issues/3527). Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp,
not the order of arrival on the device. [See #9116](https://github.com/immich-app/immich/discussions/9116).
### What happens if an asset exists in more than one account? ### What happens if an asset exists in more than one account?
@@ -308,7 +319,7 @@ Do not exaggerate with the job concurrency because you're probably thoroughly ov
### My server shows Server Status Offline | Version Unknown. What can I do? ### My server shows Server Status Offline | Version Unknown. What can I do?
You need to enable WebSockets on your reverse proxy. You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your reverse proxy.
--- ---
@@ -339,7 +350,7 @@ The non-root user/group needs read/write access to the volume mounts, including
The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts. The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts.
::: :::
For a further hardened system, you can add the following block to every container except for `immich_postgres`. For a further hardened system, you can add the following block to every container.
<details> <details>
<summary>docker-compose.yml</summary> <summary>docker-compose.yml</summary>
@@ -388,22 +399,21 @@ If the error says the worker is exiting, then this is normal. This is a feature
There are a few reasons why this can happen. There are a few reasons why this can happen.
If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory. Consider either increasing the server's RAM or moving the service to a server with more RAM. If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory.
Consider either increasing the server's RAM or moving the service to a server with more RAM.
If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible. This is unlikely to occur on version 1.92.0 or later. Consider upgrading if your version of Immich is below that. If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible with Immich.
If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
## Database ## Database
### Why am I getting database ownership errors? ### Why am I getting database ownership errors?
If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem. If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/environment-variables#supported-filesystems) for more details. NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/requirements#special-requirements-for-windows-users) for more details.
### How can I verify the integrity of my database? ### How can I verify the integrity of my database?
If you installed Immich using v1.104.0 or later, you likely have database checksums enabled by default. You can check this by running the following command. Database checksums are enabled by default for new installations since v1.104.0. You can check if they are enabled by running the following command.
A result of `on` means that checksums are enabled. A result of `on` means that checksums are enabled.
<details> <details>
@@ -419,7 +429,7 @@ docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --
</details> </details>
If checksums are enabled, you can check the status of the database with the following command. A normal result is all zeroes. If checksums are enabled, you can check the status of the database with the following command. A normal result is all `0`s.
<details> <details>
<summary>Check for database corruption</summary> <summary>Check for database corruption</summary>

View File

@@ -1,5 +1,5 @@
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk'; import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs'; import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures'; import { userDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -403,68 +403,157 @@ describe('/libraries', () => {
utils.removeImageFile(`${testAssetDir}/temp/folder} b/assetB.png`); utils.removeImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
}); });
it('should reimport a modified file', async () => { const annoyingChars = [
"'",
'"',
'`',
'*',
'{',
'}',
',',
'(',
')',
'[',
']',
'?',
'!',
'@',
'#',
'$',
'%',
'^',
'&',
'=',
'+',
'~',
'|',
'<',
'>',
';',
':',
'/', // We never got backslashes to work
];
it.each(annoyingChars)('should scan multiple import paths with %s', async (char) => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp/folder${char}1`, `${testAssetDirInternal}/temp/folder${char}2`],
}); });
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
const { status } = await request(app) const { status } = await request(app)
.post(`/libraries/${library.id}/scan`) .post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true }); .send();
expect(status).toBe(204); expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}1/asset1.png`) }),
expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}2/asset2.png`) }),
]),
);
utils.removeImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
utils.removeImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
});
it('should reimport a modified file', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/reimport`],
});
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.searchAssets(admin.accessToken, { const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id, libraryId: library.id,
model: 'NIKON D750',
}); });
expect(assets.count).toBe(1);
expect(assets.count).toEqual(1);
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(asset).toEqual(
expect.objectContaining({
originalFileName: 'asset.jpg',
exifInfo: expect.objectContaining({
model: 'NIKON D750',
}),
}),
);
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
}); });
it('should not reimport unmodified files', async () => { it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp/reimport`],
}); });
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
const { status } = await request(app) const { status } = await request(app)
.post(`/libraries/${library.id}/scan`) .post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true }); .send();
expect(status).toBe(204); expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.searchAssets(admin.accessToken, { const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id, libraryId: library.id,
model: 'NIKON D750',
}); });
expect(assets.count).toBe(0);
expect(assets.count).toEqual(1);
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(asset).toEqual(
expect.objectContaining({
originalFileName: 'asset.jpg',
exifInfo: expect.not.objectContaining({
model: 'NIKON D750',
}),
}),
);
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
}); });
it('should set an asset offline if its file is missing', async () => { it('should set an asset offline if its file is missing', async () => {
@@ -601,6 +690,298 @@ describe('/libraries', () => {
expect(assets).toEqual(assetsBefore); expect(assets).toEqual(assetsBefore);
}); });
describe('xmp metadata', async () => {
it('should import metadata from file.xmp', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should import metadata from file.ext.xmp', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should import metadata in file.ext.xmp before file.xmp if both exist', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.xmp to file.ext.xmp when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.ext.xmp to file.xmp when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.ext.xmp to file metadata', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-07-20T17:27:12.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.xmp to file metadata', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-07-20T17:27:12.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
});
}); });
describe('POST /libraries/:id/validate', () => { describe('POST /libraries/:id/validate', () => {

View File

@@ -1142,6 +1142,7 @@
"set": "Set", "set": "Set",
"set_as_album_cover": "Set as album cover", "set_as_album_cover": "Set as album cover",
"set_as_profile_picture": "Set as profile picture", "set_as_profile_picture": "Set as profile picture",
"set_as_featured_photo": "Set as featured photo",
"set_date_of_birth": "Set date of birth", "set_date_of_birth": "Set date of birth",
"set_profile_picture": "Set profile picture", "set_profile_picture": "Set profile picture",
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen", "set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
@@ -1196,6 +1197,7 @@
"sort_items": "Number of items", "sort_items": "Number of items",
"sort_modified": "Date modified", "sort_modified": "Date modified",
"sort_oldest": "Oldest photo", "sort_oldest": "Oldest photo",
"sort_people_by_similarity": "Sort people by similarity",
"sort_recent": "Most recent photo", "sort_recent": "Most recent photo",
"sort_title": "Title", "sort_title": "Title",
"source": "Source", "source": "Source",
@@ -1336,5 +1338,6 @@
"years_ago": "{years, plural, one {# year} other {# years}} ago", "years_ago": "{years, plural, one {# year} other {# years}} ago",
"yes": "Yes", "yes": "Yes",
"you_dont_have_any_shared_links": "You don't have any shared links", "you_dont_have_any_shared_links": "You don't have any shared links",
"zoom_image": "Zoom Image" "zoom_image": "Zoom Image",
"workflows": "Workflows"
} }

View File

@@ -1331,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.26.5" version = "0.27.0"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "huggingface_hub-0.26.5-py3-none-any.whl", hash = "sha256:fb7386090bbe892072e64b85f7c4479fd2d65eea5f2543327c970d5169e83924"}, {file = "huggingface_hub-0.27.0-py3-none-any.whl", hash = "sha256:8f2e834517f1f1ddf1ecc716f91b120d7333011b7485f665a9a412eacb1a2a81"},
{file = "huggingface_hub-0.26.5.tar.gz", hash = "sha256:1008bd18f60bfb65e8dbc0a97249beeeaa8c99d3c2fa649354df9fa5a13ed83b"}, {file = "huggingface_hub-0.27.0.tar.gz", hash = "sha256:902cce1a1be5739f5589e560198a65a8edcfd3b830b1666f36e4b961f0454fac"},
] ]
[package.dependencies] [package.dependencies]
@@ -2492,18 +2492,18 @@ files = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.3" version = "2.10.4"
description = "Data validation using Python type hints" description = "Data validation using Python type hints"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"},
] ]
[package.dependencies] [package.dependencies]
annotated-types = ">=0.6.0" annotated-types = ">=0.6.0"
pydantic-core = "2.27.1" pydantic-core = "2.27.2"
typing-extensions = ">=4.12.2" typing-extensions = ">=4.12.2"
[package.extras] [package.extras]
@@ -2512,111 +2512,111 @@ timezone = ["tzdata"]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.27.1" version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization" description = "Core functionality for Pydantic validation and serialization"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
] ]
[package.dependencies] [package.dependencies]
@@ -2624,13 +2624,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]] [[package]]
name = "pydantic-settings" name = "pydantic-settings"
version = "2.6.1" version = "2.7.0"
description = "Settings management using Pydantic" description = "Settings management using Pydantic"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"}, {file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"},
{file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"}, {file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"},
] ]
[package.dependencies] [package.dependencies]
@@ -2706,20 +2706,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]] [[package]]
name = "pytest-asyncio" name = "pytest-asyncio"
version = "0.24.0" version = "0.25.0"
description = "Pytest support for asyncio" description = "Pytest support for asyncio"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
files = [ files = [
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
] ]
[package.dependencies] [package.dependencies]
pytest = ">=8.2,<9" pytest = ">=8.2,<9"
[package.extras] [package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]] [[package]]
@@ -2787,13 +2787,13 @@ cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.19" version = "0.0.20"
description = "A streaming multipart parser for Python" description = "A streaming multipart parser for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
{file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
] ]
[[package]] [[package]]
@@ -3393,13 +3393,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.32.1" version = "0.34.0"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
files = [ files = [
{file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
{file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
] ]
[package.dependencies] [package.dependencies]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

View File

@@ -127,18 +127,29 @@ class GalleryViewerPage extends HookConsumerWidget {
context: context, context: context,
useSafeArea: true, useSafeArea: true,
builder: (context) { builder: (context) {
return FractionallySizedBox( return DraggableScrollableSheet(
heightFactor: 0.75, minChildSize: 0.5,
child: Padding( maxChildSize: 1,
padding: EdgeInsets.only( initialChildSize: 0.75,
bottom: context.viewInsets.bottom, expand: false,
), builder: (context, scrollController) {
child: ref return Padding(
.watch(appSettingsServiceProvider) padding: EdgeInsets.only(
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting) bottom: context.viewInsets.bottom,
? AdvancedBottomSheet(assetDetail: asset) ),
: DetailPanel(asset: asset), child: ref.watch(appSettingsServiceProvider).getSetting<bool>(
), AppSettingsEnum.advancedTroubleshooting,
)
? AdvancedBottomSheet(
assetDetail: asset,
scrollController: scrollController,
)
: DetailPanel(
asset: asset,
scrollController: scrollController,
),
);
},
); );
}, },
); );

View File

@@ -1,383 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
@RoutePage()
class OnboardingPage extends HookConsumerWidget {
const OnboardingPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final pageController = usePageController(keepPage: false);
toNextPage() {
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
return Scaffold(
appBar: AppBar(
title: SvgPicture.asset(
context.isDarkTheme
? 'assets/immich-logo-inline-dark.svg'
: 'assets/immich-logo-inline-light.svg',
height: 48,
),
centerTitle: false,
elevation: 0,
),
body: SafeArea(
child: PageView(
controller: pageController,
// physics: const NeverScrollableScrollPhysics(),
children: [
OnboardingWelcome(
onNextPage: () => toNextPage(),
),
OnboardingGalleryPermission(
onNextPage: () => toNextPage(),
),
OnboardingLocationPermission(
onNextPage: () => toNextPage(),
),
],
),
),
);
}
}
class OnboardingWelcome extends StatelessWidget {
final VoidCallback onNextPage;
const OnboardingWelcome({super.key, required this.onNextPage});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(18.0),
child: ListView(
physics: const ClampingScrollPhysics(),
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Card(
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(32),
),
),
elevation: 3,
child: AnimatedHeroImage(
imagePath: 'assets/onboarding-1-screenshot.jpeg',
color: context.colorScheme.primary.withOpacity(0.25),
colorBlendMode: BlendMode.color,
),
),
),
Padding(
padding: const EdgeInsets.only(
top: 32.0,
left: 8.0,
bottom: 8.0,
),
child: Text(
"WELCOME",
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurface.withOpacity(0.6),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
"Lets get you setup with some permissions that the app needs",
style: context.textTheme.headlineSmall,
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(right: 8.0, top: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
width: 64,
height: 64,
child: MaterialButton(
onPressed: onNextPage,
color: context.primaryColor,
textColor: Colors.white,
shape: const CircleBorder(),
child: Icon(
Icons.chevron_right_rounded,
color: context.colorScheme.onPrimary,
size: 32,
),
),
),
],
),
),
],
),
);
}
}
class AnimatedHeroImage extends StatefulWidget {
final String imagePath;
final Color color;
final BlendMode colorBlendMode;
const AnimatedHeroImage({
super.key,
required this.imagePath,
required this.color,
required this.colorBlendMode,
});
@override
AnimatedHeroImageState createState() => AnimatedHeroImageState();
}
class AnimatedHeroImageState extends State<AnimatedHeroImage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
late Animation<Offset> _parallaxAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 15),
vsync: this,
)..repeat(reverse: true);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
_rotationAnimation = Tween<double>(begin: 0.0, end: 0.025).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
_parallaxAnimation =
Tween<Offset>(begin: Offset.zero, end: const Offset(0.05, 0.05))
.animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: Image(
image: AssetImage(widget.imagePath),
filterQuality: FilterQuality.high,
isAntiAlias: true,
// color: widget.color,
// colorBlendMode: widget.colorBlendMode,
),
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: Transform.translate(
offset: _parallaxAnimation.value,
child: child,
),
),
);
},
);
}
}
class OnboardingGalleryPermission extends StatelessWidget {
final VoidCallback onNextPage;
const OnboardingGalleryPermission({super.key, required this.onNextPage});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: PermissionInfoTemplate(
icon: Icons.perm_media_outlined,
title: "Gallery Permission",
subtitle:
"We use the read and write permission of the media gallery for the following actions",
descriptionList: [
'Display the local videos and images',
'Read the file content to upload to your Immich instance',
'Remove media from the device on your request',
],
onConfirm: onNextPage,
),
);
}
}
class OnboardingLocationPermission extends StatelessWidget {
final VoidCallback onNextPage;
const OnboardingLocationPermission({super.key, required this.onNextPage});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: PermissionInfoTemplate(
icon: Icons.location_on_outlined,
title: "Location Permission",
subtitle:
"We use the always on, precise location access for the following actions",
descriptionList: [
'Display the local videos and images',
'Read the file content to upload to your Immich instance',
'Remove media from the device on your request',
],
onConfirm: onNextPage,
),
);
}
}
class PermissionInfoTemplate extends StatelessWidget {
final String title;
final String subtitle;
final List<String> descriptionList;
final VoidCallback onConfirm;
final IconData icon;
const PermissionInfoTemplate({
super.key,
required this.title,
required this.subtitle,
required this.descriptionList,
required this.onConfirm,
required this.icon,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(
icon,
size: 32,
color: context.primaryColor.withAlpha(250),
),
const SizedBox(width: 16),
Text(
title,
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
),
],
),
const SizedBox(height: 16),
Text(
subtitle,
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w400,
color: context.colorScheme.onSurface.withAlpha(220),
),
),
const SizedBox(height: 40),
BulletList(descriptionList),
const Spacer(),
SizedBox(
height: 48,
width: double.infinity,
child: ElevatedButton(
onPressed: onConfirm,
child: const Text(
'OK',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
],
);
}
}
class BulletList extends StatelessWidget {
final List<String> strings;
const BulletList(this.strings, {super.key});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: strings.map((textString) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'\u2022',
style: TextStyle(
fontSize: 20,
height: 1.25,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
textString,
textAlign: TextAlign.left,
softWrap: true,
style: context.textTheme.headlineSmall?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}).toList(),
),
);
}
}

View File

@@ -41,7 +41,6 @@ import 'package:immich_mobile/pages/library/favorite.page.dart';
import 'package:immich_mobile/pages/library/trash.page.dart'; import 'package:immich_mobile/pages/library/trash.page.dart';
import 'package:immich_mobile/pages/login/change_password.page.dart'; import 'package:immich_mobile/pages/login/change_password.page.dart';
import 'package:immich_mobile/pages/login/login.page.dart'; import 'package:immich_mobile/pages/login/login.page.dart';
import 'package:immich_mobile/pages/onboarding/onboarding.page.dart';
import 'package:immich_mobile/pages/onboarding/permission_onboarding.page.dart'; import 'package:immich_mobile/pages/onboarding/permission_onboarding.page.dart';
import 'package:immich_mobile/pages/photos/memory.page.dart'; import 'package:immich_mobile/pages/photos/memory.page.dart';
import 'package:immich_mobile/pages/photos/photos.page.dart'; import 'package:immich_mobile/pages/photos/photos.page.dart';
@@ -91,8 +90,7 @@ class AppRouter extends RootStackRouter {
@override @override
late final List<AutoRoute> routes = [ late final List<AutoRoute> routes = [
AutoRoute(page: OnboardingRoute.page, initial: true), AutoRoute(page: SplashScreenRoute.page, initial: true),
AutoRoute(page: SplashScreenRoute.page),
AutoRoute( AutoRoute(
page: PermissionOnboardingRoute.page, page: PermissionOnboardingRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],

View File

@@ -136,10 +136,15 @@ class AlbumAssetSelectionRouteArgs {
/// generated route for /// generated route for
/// [AlbumOptionsPage] /// [AlbumOptionsPage]
class AlbumOptionsRoute extends PageRouteInfo<void> { class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
const AlbumOptionsRoute({List<PageRouteInfo>? children}) AlbumOptionsRoute({
: super( Key? key,
List<PageRouteInfo>? children,
}) : super(
AlbumOptionsRoute.name, AlbumOptionsRoute.name,
args: AlbumOptionsRouteArgs(
key: key,
),
initialChildren: children, initialChildren: children,
); );
@@ -148,11 +153,25 @@ class AlbumOptionsRoute extends PageRouteInfo<void> {
static PageInfo page = PageInfo( static PageInfo page = PageInfo(
name, name,
builder: (data) { builder: (data) {
return const AlbumOptionsPage(); final args = data.argsAs<AlbumOptionsRouteArgs>();
return AlbumOptionsPage(
key: args.key,
);
}, },
); );
} }
class AlbumOptionsRouteArgs {
const AlbumOptionsRouteArgs({this.key});
final Key? key;
@override
String toString() {
return 'AlbumOptionsRouteArgs{key: $key}';
}
}
/// generated route for /// generated route for
/// [AlbumPreviewPage] /// [AlbumPreviewPage]
class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> { class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
@@ -1110,25 +1129,6 @@ class NativeVideoViewerRouteArgs {
} }
} }
/// generated route for
/// [OnboardingPage]
class OnboardingRoute extends PageRouteInfo<void> {
const OnboardingRoute({List<PageRouteInfo>? children})
: super(
OnboardingRoute.name,
initialChildren: children,
);
static const String name = 'OnboardingRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const OnboardingPage();
},
);
}
/// generated route for /// generated route for
/// [PartnerDetailPage] /// [PartnerDetailPage]
class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> { class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> {

View File

@@ -372,7 +372,6 @@ class BackgroundService {
HttpOverrides.global = HttpSSLCertOverride(); HttpOverrides.global = HttpSSLCertOverride();
ApiService apiService = ApiService(); ApiService apiService = ApiService();
apiService.setAccessToken(Store.get(StoreKey.accessToken)); apiService.setAccessToken(Store.get(StoreKey.accessToken));
AppSettingsService settingService = AppSettingsService();
AppSettingsService settingsService = AppSettingsService(); AppSettingsService settingsService = AppSettingsService();
AlbumRepository albumRepository = AlbumRepository(db); AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db); AssetRepository assetRepository = AssetRepository(db);
@@ -422,7 +421,7 @@ class BackgroundService {
); );
BackupService backupService = BackupService( BackupService backupService = BackupService(
apiService, apiService,
settingService, settingsService,
albumService, albumService,
albumMediaRepository, albumMediaRepository,
fileMediaRepository, fileMediaRepository,

View File

@@ -313,15 +313,12 @@ class BackupService {
); );
} }
} else { } else {
if (asset.type == AssetType.video) { file =
file = await asset.local!.originFile; await asset.local!.originFile.timeout(const Duration(seconds: 5));
} else {
file = await asset.local!.originFile if (asset.local!.isLivePhoto) {
livePhotoFile = await asset.local!.originFileWithSubtype
.timeout(const Duration(seconds: 5)); .timeout(const Duration(seconds: 5));
if (asset.local!.isLivePhoto) {
livePhotoFile = await asset.local!.originFileWithSubtype
.timeout(const Duration(seconds: 5));
}
} }
} }

View File

@@ -6,12 +6,18 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class AdvancedBottomSheet extends HookConsumerWidget { class AdvancedBottomSheet extends HookConsumerWidget {
final Asset assetDetail; final Asset assetDetail;
final ScrollController? scrollController;
const AdvancedBottomSheet({super.key, required this.assetDetail}); const AdvancedBottomSheet({
super.key,
required this.assetDetail,
this.scrollController,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return SingleChildScrollView( return SingleChildScrollView(
controller: scrollController,
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0), margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: LayoutBuilder( child: LayoutBuilder(

View File

@@ -9,12 +9,14 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class DetailPanel extends HookConsumerWidget { class DetailPanel extends HookConsumerWidget {
final Asset asset; final Asset asset;
final ScrollController? scrollController;
const DetailPanel({super.key, required this.asset}); const DetailPanel({super.key, required this.asset, this.scrollController});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return ListView( return ListView(
controller: scrollController,
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
Padding( Padding(

View File

@@ -18,7 +18,7 @@ class FileInfo extends StatelessWidget {
final height = asset.orientatedHeight ?? asset.height; final height = asset.orientatedHeight ?? asset.height;
final width = asset.orientatedWidth ?? asset.width; final width = asset.orientatedWidth ?? asset.width;
String resolution = String resolution =
height != null && width != null ? "$height x $width " : ""; height != null && width != null ? "$width x $height " : "";
String fileSize = asset.exifInfo?.fileSize != null String fileSize = asset.exifInfo?.fileSize != null
? formatBytes(asset.exifInfo!.fileSize!) ? formatBytes(asset.exifInfo!.fileSize!)
: ""; : "";

View File

@@ -93,17 +93,17 @@ Class | Method | HTTP request | Description
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} |
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | *AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} |
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Checks if assets exist by checksums
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | *AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Checks if multiple assets exist on the server and returns all existing - used by background backup
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Get all asset of a device that are in the database, ID only.
*AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} |
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |
*AssetsApi* | [**getMemoryLane**](doc//AssetsApi.md#getmemorylane) | **GET** /assets/memory-lane | *AssetsApi* | [**getMemoryLane**](doc//AssetsApi.md#getmemorylane) | **GET** /assets/memory-lane |
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
*AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |

View File

@@ -13,17 +13,11 @@ part of openapi.api;
class AlbumUserAddDto { class AlbumUserAddDto {
/// Returns a new [AlbumUserAddDto] instance. /// Returns a new [AlbumUserAddDto] instance.
AlbumUserAddDto({ AlbumUserAddDto({
this.role, this.role = AlbumUserRole.editor,
required this.userId, required this.userId,
}); });
/// AlbumUserRole role;
/// 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.
///
AlbumUserRole? role;
String userId; String userId;
@@ -35,7 +29,7 @@ class AlbumUserAddDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(role == null ? 0 : role!.hashCode) + (role.hashCode) +
(userId.hashCode); (userId.hashCode);
@override @override
@@ -43,11 +37,7 @@ class AlbumUserAddDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.role != null) {
json[r'role'] = this.role; json[r'role'] = this.role;
} else {
// json[r'role'] = null;
}
json[r'userId'] = this.userId; json[r'userId'] = this.userId;
return json; return json;
} }
@@ -61,7 +51,7 @@ class AlbumUserAddDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AlbumUserAddDto( return AlbumUserAddDto(
role: AlbumUserRole.fromJson(json[r'role']), role: AlbumUserRole.fromJson(json[r'role']) ?? AlbumUserRole.editor,
userId: mapValueOfType<String>(json, r'userId')!, userId: mapValueOfType<String>(json, r'userId')!,
); );
} }

View File

@@ -13,15 +13,15 @@ part of openapi.api;
class CreateLibraryDto { class CreateLibraryDto {
/// Returns a new [CreateLibraryDto] instance. /// Returns a new [CreateLibraryDto] instance.
CreateLibraryDto({ CreateLibraryDto({
this.exclusionPatterns = const [], this.exclusionPatterns = const {},
this.importPaths = const [], this.importPaths = const {},
this.name, this.name,
required this.ownerId, required this.ownerId,
}); });
List<String> exclusionPatterns; Set<String> exclusionPatterns;
List<String> importPaths; Set<String> importPaths;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@@ -53,8 +53,8 @@ class CreateLibraryDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false);
json[r'importPaths'] = this.importPaths; json[r'importPaths'] = this.importPaths.toList(growable: false);
if (this.name != null) { if (this.name != null) {
json[r'name'] = this.name; json[r'name'] = this.name;
} else { } else {
@@ -74,11 +74,11 @@ class CreateLibraryDto {
return CreateLibraryDto( return CreateLibraryDto(
exclusionPatterns: json[r'exclusionPatterns'] is Iterable exclusionPatterns: json[r'exclusionPatterns'] is Iterable
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false) ? (json[r'exclusionPatterns'] as Iterable).cast<String>().toSet()
: const [], : const {},
importPaths: json[r'importPaths'] is Iterable importPaths: json[r'importPaths'] is Iterable
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false) ? (json[r'importPaths'] as Iterable).cast<String>().toSet()
: const [], : const {},
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
); );

View File

@@ -13,14 +13,14 @@ part of openapi.api;
class UpdateLibraryDto { class UpdateLibraryDto {
/// Returns a new [UpdateLibraryDto] instance. /// Returns a new [UpdateLibraryDto] instance.
UpdateLibraryDto({ UpdateLibraryDto({
this.exclusionPatterns = const [], this.exclusionPatterns = const {},
this.importPaths = const [], this.importPaths = const {},
this.name, this.name,
}); });
List<String> exclusionPatterns; Set<String> exclusionPatterns;
List<String> importPaths; Set<String> importPaths;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@@ -48,8 +48,8 @@ class UpdateLibraryDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false);
json[r'importPaths'] = this.importPaths; json[r'importPaths'] = this.importPaths.toList(growable: false);
if (this.name != null) { if (this.name != null) {
json[r'name'] = this.name; json[r'name'] = this.name;
} else { } else {
@@ -68,11 +68,11 @@ class UpdateLibraryDto {
return UpdateLibraryDto( return UpdateLibraryDto(
exclusionPatterns: json[r'exclusionPatterns'] is Iterable exclusionPatterns: json[r'exclusionPatterns'] is Iterable
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false) ? (json[r'exclusionPatterns'] as Iterable).cast<String>().toSet()
: const [], : const {},
importPaths: json[r'importPaths'] is Iterable importPaths: json[r'importPaths'] is Iterable
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false) ? (json[r'importPaths'] as Iterable).cast<String>().toSet()
: const [], : const {},
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
); );
} }

View File

@@ -13,13 +13,13 @@ part of openapi.api;
class ValidateLibraryDto { class ValidateLibraryDto {
/// Returns a new [ValidateLibraryDto] instance. /// Returns a new [ValidateLibraryDto] instance.
ValidateLibraryDto({ ValidateLibraryDto({
this.exclusionPatterns = const [], this.exclusionPatterns = const {},
this.importPaths = const [], this.importPaths = const {},
}); });
List<String> exclusionPatterns; Set<String> exclusionPatterns;
List<String> importPaths; Set<String> importPaths;
@override @override
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto && bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto &&
@@ -37,8 +37,8 @@ class ValidateLibraryDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false);
json[r'importPaths'] = this.importPaths; json[r'importPaths'] = this.importPaths.toList(growable: false);
return json; return json;
} }
@@ -52,11 +52,11 @@ class ValidateLibraryDto {
return ValidateLibraryDto( return ValidateLibraryDto(
exclusionPatterns: json[r'exclusionPatterns'] is Iterable exclusionPatterns: json[r'exclusionPatterns'] is Iterable
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false) ? (json[r'exclusionPatterns'] as Iterable).cast<String>().toSet()
: const [], : const {},
importPaths: json[r'importPaths'] is Iterable importPaths: json[r'importPaths'] is Iterable
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false) ? (json[r'importPaths'] as Iterable).cast<String>().toSet()
: const [], : const {},
); );
} }
return null; return null;

View File

@@ -1424,7 +1424,6 @@
}, },
"/assets/bulk-upload-check": { "/assets/bulk-upload-check": {
"post": { "post": {
"description": "Checks if assets exist by checksums",
"operationId": "checkBulkUpload", "operationId": "checkBulkUpload",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
@@ -1460,6 +1459,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "Checks if assets exist by checksums",
"tags": [ "tags": [
"Assets" "Assets"
] ]
@@ -1467,7 +1467,6 @@
}, },
"/assets/device/{deviceId}": { "/assets/device/{deviceId}": {
"get": { "get": {
"description": "Get all asset of a device that are in the database, ID only.",
"operationId": "getAllUserAssetsByDeviceId", "operationId": "getAllUserAssetsByDeviceId",
"parameters": [ "parameters": [
{ {
@@ -1505,6 +1504,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "Get all asset of a device that are in the database, ID only.",
"tags": [ "tags": [
"Assets" "Assets"
] ]
@@ -1512,7 +1512,6 @@
}, },
"/assets/exist": { "/assets/exist": {
"post": { "post": {
"description": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"operationId": "checkExistingAssets", "operationId": "checkExistingAssets",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
@@ -1548,6 +1547,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"tags": [ "tags": [
"Assets" "Assets"
] ]
@@ -1903,7 +1903,6 @@
] ]
}, },
"put": { "put": {
"description": "Replace the asset with new file, without changing its id",
"operationId": "replaceAsset", "operationId": "replaceAsset",
"parameters": [ "parameters": [
{ {
@@ -1957,6 +1956,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "Replace the asset with new file, without changing its id",
"tags": [ "tags": [
"Assets" "Assets"
], ],
@@ -7492,6 +7492,7 @@
"items": { "items": {
"$ref": "#/components/schemas/Permission" "$ref": "#/components/schemas/Permission"
}, },
"minItems": 1,
"type": "array" "type": "array"
} }
}, },
@@ -7572,7 +7573,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/ReactionType" "allOf": [
{
"$ref": "#/components/schemas/ReactionType"
}
]
} }
}, },
"required": [ "required": [
@@ -7599,7 +7604,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/ReactionType" "allOf": [
{
"$ref": "#/components/schemas/ReactionType"
}
]
}, },
"user": { "user": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
@@ -7631,6 +7640,7 @@
"items": { "items": {
"$ref": "#/components/schemas/AlbumUserAddDto" "$ref": "#/components/schemas/AlbumUserAddDto"
}, },
"minItems": 1,
"type": "array" "type": "array"
} }
}, },
@@ -7699,7 +7709,11 @@
"type": "string" "type": "string"
}, },
"order": { "order": {
"$ref": "#/components/schemas/AssetOrder" "allOf": [
{
"$ref": "#/components/schemas/AssetOrder"
}
]
}, },
"owner": { "owner": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
@@ -7759,7 +7773,12 @@
"AlbumUserAddDto": { "AlbumUserAddDto": {
"properties": { "properties": {
"role": { "role": {
"$ref": "#/components/schemas/AlbumUserRole" "allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
],
"default": "editor"
}, },
"userId": { "userId": {
"format": "uuid", "format": "uuid",
@@ -7774,7 +7793,11 @@
"AlbumUserCreateDto": { "AlbumUserCreateDto": {
"properties": { "properties": {
"role": { "role": {
"$ref": "#/components/schemas/AlbumUserRole" "allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
]
}, },
"userId": { "userId": {
"format": "uuid", "format": "uuid",
@@ -7790,7 +7813,11 @@
"AlbumUserResponseDto": { "AlbumUserResponseDto": {
"properties": { "properties": {
"role": { "role": {
"$ref": "#/components/schemas/AlbumUserRole" "allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
]
}, },
"user": { "user": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
@@ -8087,7 +8114,11 @@
"nullable": true "nullable": true
}, },
"sourceType": { "sourceType": {
"$ref": "#/components/schemas/SourceType" "allOf": [
{
"$ref": "#/components/schemas/SourceType"
}
]
} }
}, },
"required": [ "required": [
@@ -8158,7 +8189,11 @@
"type": "integer" "type": "integer"
}, },
"sourceType": { "sourceType": {
"$ref": "#/components/schemas/SourceType" "allOf": [
{
"$ref": "#/components/schemas/SourceType"
}
]
} }
}, },
"required": [ "required": [
@@ -8254,7 +8289,11 @@
"type": "array" "type": "array"
}, },
"name": { "name": {
"$ref": "#/components/schemas/AssetJobName" "allOf": [
{
"$ref": "#/components/schemas/AssetJobName"
}
]
} }
}, },
"required": [ "required": [
@@ -8352,7 +8391,11 @@
"type": "string" "type": "string"
}, },
"status": { "status": {
"$ref": "#/components/schemas/AssetMediaStatus" "allOf": [
{
"$ref": "#/components/schemas/AssetMediaStatus"
}
]
} }
}, },
"required": [ "required": [
@@ -8490,7 +8533,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/AssetTypeEnum" "allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
}, },
"unassignedFaces": { "unassignedFaces": {
"items": { "items": {
@@ -8603,7 +8650,11 @@
"AvatarResponse": { "AvatarResponse": {
"properties": { "properties": {
"color": { "color": {
"$ref": "#/components/schemas/UserAvatarColor" "allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
} }
}, },
"required": [ "required": [
@@ -8614,7 +8665,11 @@
"AvatarUpdate": { "AvatarUpdate": {
"properties": { "properties": {
"color": { "color": {
"$ref": "#/components/schemas/UserAvatarColor" "allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
} }
}, },
"type": "object" "type": "object"
@@ -8705,6 +8760,7 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"minItems": 1,
"type": "array" "type": "array"
}, },
"deviceId": { "deviceId": {
@@ -8771,13 +8827,17 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"type": "array" "maxItems": 128,
"type": "array",
"uniqueItems": true
}, },
"importPaths": { "importPaths": {
"items": { "items": {
"type": "string" "type": "string"
}, },
"type": "array" "maxItems": 128,
"type": "array",
"uniqueItems": true
}, },
"name": { "name": {
"type": "string" "type": "string"
@@ -9246,10 +9306,18 @@
"type": "string" "type": "string"
}, },
"entityType": { "entityType": {
"$ref": "#/components/schemas/PathEntityType" "allOf": [
{
"$ref": "#/components/schemas/PathEntityType"
}
]
}, },
"pathType": { "pathType": {
"$ref": "#/components/schemas/PathType" "allOf": [
{
"$ref": "#/components/schemas/PathType"
}
]
}, },
"pathValue": { "pathValue": {
"type": "string" "type": "string"
@@ -9311,7 +9379,11 @@
"JobCommandDto": { "JobCommandDto": {
"properties": { "properties": {
"command": { "command": {
"$ref": "#/components/schemas/JobCommand" "allOf": [
{
"$ref": "#/components/schemas/JobCommand"
}
]
}, },
"force": { "force": {
"type": "boolean" "type": "boolean"
@@ -9356,7 +9428,11 @@
"JobCreateDto": { "JobCreateDto": {
"properties": { "properties": {
"name": { "name": {
"$ref": "#/components/schemas/ManualJobName" "allOf": [
{
"$ref": "#/components/schemas/ManualJobName"
}
]
} }
}, },
"required": [ "required": [
@@ -9544,6 +9620,7 @@
"properties": { "properties": {
"email": { "email": {
"example": "testuser@email.com", "example": "testuser@email.com",
"format": "email",
"type": "string" "type": "string"
}, },
"password": { "password": {
@@ -9717,7 +9794,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/MemoryType" "allOf": [
{
"$ref": "#/components/schemas/MemoryType"
}
]
} }
}, },
"required": [ "required": [
@@ -9782,7 +9863,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/MemoryType" "allOf": [
{
"$ref": "#/components/schemas/MemoryType"
}
]
}, },
"updatedAt": { "updatedAt": {
"format": "date-time", "format": "date-time",
@@ -9911,7 +9996,11 @@
"type": "string" "type": "string"
}, },
"order": { "order": {
"$ref": "#/components/schemas/AssetOrder" "allOf": [
{
"$ref": "#/components/schemas/AssetOrder"
}
]
}, },
"originalFileName": { "originalFileName": {
"type": "string" "type": "string"
@@ -9962,7 +10051,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/AssetTypeEnum" "allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
}, },
"updatedAfter": { "updatedAfter": {
"format": "date-time", "format": "date-time",
@@ -10046,7 +10139,11 @@
"PartnerResponseDto": { "PartnerResponseDto": {
"properties": { "properties": {
"avatarColor": { "avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor" "allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
}, },
"email": { "email": {
"type": "string" "type": "string"
@@ -10564,7 +10661,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/AssetTypeEnum" "allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
}, },
"updatedAfter": { "updatedAfter": {
"format": "date-time", "format": "date-time",
@@ -11232,7 +11333,11 @@
"type": "boolean" "type": "boolean"
}, },
"type": { "type": {
"$ref": "#/components/schemas/SharedLinkType" "allOf": [
{
"$ref": "#/components/schemas/SharedLinkType"
}
]
} }
}, },
"required": [ "required": [
@@ -11317,7 +11422,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/SharedLinkType" "allOf": [
{
"$ref": "#/components/schemas/SharedLinkType"
}
]
}, },
"userId": { "userId": {
"type": "string" "type": "string"
@@ -11350,6 +11459,7 @@
"properties": { "properties": {
"email": { "email": {
"example": "testuser@email.com", "example": "testuser@email.com",
"format": "email",
"type": "string" "type": "string"
}, },
"name": { "name": {
@@ -11466,7 +11576,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"$ref": "#/components/schemas/AssetTypeEnum" "allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
}, },
"updatedAfter": { "updatedAfter": {
"format": "date-time", "format": "date-time",
@@ -11507,6 +11621,7 @@
"format": "uuid", "format": "uuid",
"type": "string" "type": "string"
}, },
"minItems": 2,
"type": "array" "type": "array"
} }
}, },
@@ -11647,7 +11762,11 @@
"SystemConfigFFmpegDto": { "SystemConfigFFmpegDto": {
"properties": { "properties": {
"accel": { "accel": {
"$ref": "#/components/schemas/TranscodeHWAccel" "allOf": [
{
"$ref": "#/components/schemas/TranscodeHWAccel"
}
]
}, },
"accelDecode": { "accelDecode": {
"type": "boolean" "type": "boolean"
@@ -11676,7 +11795,11 @@
"type": "integer" "type": "integer"
}, },
"cqMode": { "cqMode": {
"$ref": "#/components/schemas/CQMode" "allOf": [
{
"$ref": "#/components/schemas/CQMode"
}
]
}, },
"crf": { "crf": {
"maximum": 51, "maximum": 51,
@@ -11702,13 +11825,21 @@
"type": "integer" "type": "integer"
}, },
"targetAudioCodec": { "targetAudioCodec": {
"$ref": "#/components/schemas/AudioCodec" "allOf": [
{
"$ref": "#/components/schemas/AudioCodec"
}
]
}, },
"targetResolution": { "targetResolution": {
"type": "string" "type": "string"
}, },
"targetVideoCodec": { "targetVideoCodec": {
"$ref": "#/components/schemas/VideoCodec" "allOf": [
{
"$ref": "#/components/schemas/VideoCodec"
}
]
}, },
"temporalAQ": { "temporalAQ": {
"type": "boolean" "type": "boolean"
@@ -11718,10 +11849,18 @@
"type": "integer" "type": "integer"
}, },
"tonemap": { "tonemap": {
"$ref": "#/components/schemas/ToneMapping" "allOf": [
{
"$ref": "#/components/schemas/ToneMapping"
}
]
}, },
"transcode": { "transcode": {
"$ref": "#/components/schemas/TranscodePolicy" "allOf": [
{
"$ref": "#/components/schemas/TranscodePolicy"
}
]
}, },
"twoPass": { "twoPass": {
"type": "boolean" "type": "boolean"
@@ -11766,7 +11905,11 @@
"SystemConfigGeneratedImageDto": { "SystemConfigGeneratedImageDto": {
"properties": { "properties": {
"format": { "format": {
"$ref": "#/components/schemas/ImageFormat" "allOf": [
{
"$ref": "#/components/schemas/ImageFormat"
}
]
}, },
"quality": { "quality": {
"maximum": 100, "maximum": 100,
@@ -11788,7 +11931,11 @@
"SystemConfigImageDto": { "SystemConfigImageDto": {
"properties": { "properties": {
"colorspace": { "colorspace": {
"$ref": "#/components/schemas/Colorspace" "allOf": [
{
"$ref": "#/components/schemas/Colorspace"
}
]
}, },
"extractEmbedded": { "extractEmbedded": {
"type": "boolean" "type": "boolean"
@@ -11906,7 +12053,11 @@
"type": "boolean" "type": "boolean"
}, },
"level": { "level": {
"$ref": "#/components/schemas/LogLevel" "allOf": [
{
"$ref": "#/components/schemas/LogLevel"
}
]
} }
}, },
"required": [ "required": [
@@ -11935,6 +12086,7 @@
"type": "string" "type": "string"
}, },
"urls": { "urls": {
"format": "uri",
"items": { "items": {
"format": "uri", "format": "uri",
"type": "string" "type": "string"
@@ -11955,12 +12107,14 @@
"SystemConfigMapDto": { "SystemConfigMapDto": {
"properties": { "properties": {
"darkStyle": { "darkStyle": {
"format": "uri",
"type": "string" "type": "string"
}, },
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
}, },
"lightStyle": { "lightStyle": {
"format": "uri",
"type": "string" "type": "string"
} }
}, },
@@ -12035,6 +12189,7 @@
"type": "boolean" "type": "boolean"
}, },
"mobileRedirectUri": { "mobileRedirectUri": {
"format": "uri",
"type": "string" "type": "string"
}, },
"profileSigningAlgorithm": { "profileSigningAlgorithm": {
@@ -12097,6 +12252,7 @@
"SystemConfigServerDto": { "SystemConfigServerDto": {
"properties": { "properties": {
"externalDomain": { "externalDomain": {
"format": "uri",
"type": "string" "type": "string"
}, },
"loginPageMessage": { "loginPageMessage": {
@@ -12353,6 +12509,7 @@
"TagCreateDto": { "TagCreateDto": {
"properties": { "properties": {
"color": { "color": {
"pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
"type": "string" "type": "string"
}, },
"name": { "name": {
@@ -12408,6 +12565,7 @@
"properties": { "properties": {
"color": { "color": {
"nullable": true, "nullable": true,
"pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
"type": "string" "type": "string"
} }
}, },
@@ -12570,7 +12728,11 @@
"type": "boolean" "type": "boolean"
}, },
"order": { "order": {
"$ref": "#/components/schemas/AssetOrder" "allOf": [
{
"$ref": "#/components/schemas/AssetOrder"
}
]
} }
}, },
"type": "object" "type": "object"
@@ -12578,7 +12740,11 @@
"UpdateAlbumUserDto": { "UpdateAlbumUserDto": {
"properties": { "properties": {
"role": { "role": {
"$ref": "#/components/schemas/AlbumUserRole" "allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
]
} }
}, },
"required": [ "required": [
@@ -12625,13 +12791,17 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"type": "array" "maxItems": 128,
"type": "array",
"uniqueItems": true
}, },
"importPaths": { "importPaths": {
"items": { "items": {
"type": "string" "type": "string"
}, },
"type": "array" "maxItems": 128,
"type": "array",
"uniqueItems": true
}, },
"name": { "name": {
"type": "string" "type": "string"
@@ -12697,6 +12867,7 @@
"UserAdminCreateDto": { "UserAdminCreateDto": {
"properties": { "properties": {
"email": { "email": {
"format": "email",
"type": "string" "type": "string"
}, },
"name": { "name": {
@@ -12740,7 +12911,11 @@
"UserAdminResponseDto": { "UserAdminResponseDto": {
"properties": { "properties": {
"avatarColor": { "avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor" "allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
}, },
"createdAt": { "createdAt": {
"format": "date-time", "format": "date-time",
@@ -12795,7 +12970,11 @@
"type": "boolean" "type": "boolean"
}, },
"status": { "status": {
"$ref": "#/components/schemas/UserStatus" "allOf": [
{
"$ref": "#/components/schemas/UserStatus"
}
]
}, },
"storageLabel": { "storageLabel": {
"nullable": true, "nullable": true,
@@ -12830,6 +13009,7 @@
"UserAdminUpdateDto": { "UserAdminUpdateDto": {
"properties": { "properties": {
"email": { "email": {
"format": "email",
"type": "string" "type": "string"
}, },
"name": { "name": {
@@ -12967,7 +13147,11 @@
"UserResponseDto": { "UserResponseDto": {
"properties": { "properties": {
"avatarColor": { "avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor" "allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
}, },
"email": { "email": {
"type": "string" "type": "string"
@@ -13007,6 +13191,7 @@
"UserUpdateMeDto": { "UserUpdateMeDto": {
"properties": { "properties": {
"email": { "email": {
"format": "email",
"type": "string" "type": "string"
}, },
"name": { "name": {
@@ -13035,13 +13220,17 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"type": "array" "maxItems": 128,
"type": "array",
"uniqueItems": true
}, },
"importPaths": { "importPaths": {
"items": { "items": {
"type": "string" "type": "string"
}, },
"type": "array" "maxItems": 128,
"type": "array",
"uniqueItems": true
} }
}, },
"type": "object" "type": "object"

View File

@@ -36,6 +36,7 @@
<a href="README_pt_BR.md">Português Brasileiro</a> <a href="README_pt_BR.md">Português Brasileiro</a>
<a href="README_sv_SE.md">Svenska</a> <a href="README_sv_SE.md">Svenska</a>
<a href="README_ar_JO.md">العربية</a> <a href="README_ar_JO.md">العربية</a>
<a href="README_vi_VN.md">Tiếng Việt</a>
<a href="README_th_TH.md">ภาษาไทย</a> <a href="README_th_TH.md">ภาษาไทย</a>
</p> </p>
@@ -105,6 +106,8 @@
| 离线支持 | 是 | 否 | | 离线支持 | 是 | 否 |
| 只读相册 | 是 | 是 | | 只读相册 | 是 | 是 |
| 照片堆叠 | 是 | 是 | | 照片堆叠 | 是 | 是 |
| 标签 | 否 | 是 |
| 文件夹浏览 | 否 | 是 |
## 多语言 ## 多语言

View File

@@ -1,5 +1,5 @@
# dev build # dev build
FROM ghcr.io/immich-app/base-server-dev:20241217@sha256:7e69fa317cf90a0345927bbea13438dc39efc584bac13ff77ea5735c57cd008a AS dev FROM ghcr.io/immich-app/base-server-dev:20241224@sha256:6832c632c2a8cba5e20053ab694c9a8080e621841c784ed5d4675ef9dd203588 AS dev
RUN apt-get install --no-install-recommends -yqq tini RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -42,7 +42,7 @@ RUN npm run build
# prod build # prod build
FROM ghcr.io/immich-app/base-server-prod:20241217@sha256:040c83a6d3e45755419837747fa70fa68cf92433d483c116a971b3400bb8415d FROM ghcr.io/immich-app/base-server-prod:20241224@sha256:69da007c241a961d6927d3d03f1c83ef0ec5c70bf656bff3ced32546a777e6f6
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production \ ENV NODE_ENV=production \

View File

@@ -16,7 +16,7 @@
"@nestjs/platform-express": "^10.2.2", "@nestjs/platform-express": "^10.2.2",
"@nestjs/platform-socket.io": "^10.2.2", "@nestjs/platform-socket.io": "^10.2.2",
"@nestjs/schedule": "^4.0.0", "@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.8", "@nestjs/swagger": "^8.0.0",
"@nestjs/typeorm": "^10.0.0", "@nestjs/typeorm": "^10.0.0",
"@nestjs/websockets": "^10.2.2", "@nestjs/websockets": "^10.2.2",
"@opentelemetry/auto-instrumentations-node": "^0.54.0", "@opentelemetry/auto-instrumentations-node": "^0.54.0",
@@ -2099,9 +2099,9 @@
} }
}, },
"node_modules/@nestjs/mapped-types": { "node_modules/@nestjs/mapped-types": {
"version": "2.0.5", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz",
"integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
@@ -2197,17 +2197,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@nestjs/swagger": { "node_modules/@nestjs/swagger": {
"version": "7.4.2", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.1.0.tgz",
"integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", "integrity": "sha512-8hzH+r/31XshzXHC9vww4T0xjDAxMzvOaT1xAOvvY1LtXTWyNRCUP2iQsCYJOnnMrR+vydWjvRZiuB3hdvaHxA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@microsoft/tsdoc": "^0.15.0", "@microsoft/tsdoc": "^0.15.0",
"@nestjs/mapped-types": "2.0.5", "@nestjs/mapped-types": "2.0.6",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"path-to-regexp": "3.3.0", "path-to-regexp": "3.3.0",
"swagger-ui-dist": "5.17.14" "swagger-ui-dist": "5.18.2"
}, },
"peerDependencies": { "peerDependencies": {
"@fastify/static": "^6.0.0 || ^7.0.0", "@fastify/static": "^6.0.0 || ^7.0.0",
@@ -4464,6 +4464,13 @@
"win32" "win32"
] ]
}, },
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@selderee/plugin-htmlparser2": { "node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
@@ -13764,10 +13771,13 @@
} }
}, },
"node_modules/swagger-ui-dist": { "node_modules/swagger-ui-dist": {
"version": "5.17.14", "version": "5.18.2",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz",
"integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==",
"license": "Apache-2.0" "license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
}, },
"node_modules/symbol-observable": { "node_modules/symbol-observable": {
"version": "4.0.0", "version": "4.0.0",

View File

@@ -41,7 +41,7 @@
"@nestjs/platform-express": "^10.2.2", "@nestjs/platform-express": "^10.2.2",
"@nestjs/platform-socket.io": "^10.2.2", "@nestjs/platform-socket.io": "^10.2.2",
"@nestjs/schedule": "^4.0.0", "@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.8", "@nestjs/swagger": "^8.0.0",
"@nestjs/typeorm": "^10.0.0", "@nestjs/typeorm": "^10.0.0",
"@nestjs/websockets": "^10.2.2", "@nestjs/websockets": "^10.2.2",
"@opentelemetry/auto-instrumentations-node": "^0.54.0", "@opentelemetry/auto-instrumentations-node": "^0.54.0",

View File

@@ -204,7 +204,7 @@ describe('getEnv', () => {
it('should return default network options', () => { it('should return default network options', () => {
const { network } = getEnv(); const { network } = getEnv();
expect(network).toEqual({ expect(network).toEqual({
trustedProxies: [], trustedProxies: ['linklocal', 'uniquelocal'],
}); });
}); });

View File

@@ -177,7 +177,7 @@ const getEnv = (): EnvData => {
licensePublicKey: isProd ? productionKeys : stagingKeys, licensePublicKey: isProd ? productionKeys : stagingKeys,
network: { network: {
trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? [], trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? ['linklocal', 'uniquelocal'],
}, },
otel: { otel: {

View File

@@ -214,7 +214,7 @@ export class StorageRepository implements IStorageRepository {
} }
private asGlob(pathToCrawl: string): string { private asGlob(pathToCrawl: string): string {
const escapedPath = escapePath(pathToCrawl); const escapedPath = escapePath(pathToCrawl).replaceAll('"', '["]').replaceAll("'", "[']").replaceAll('`', '[`]');
const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`; const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`;
return `${escapedPath}/**/${extensions}`; return `${escapedPath}/**/${extensions}`;
} }

View File

@@ -414,7 +414,6 @@ describe(LibraryService.name, () => {
localDateTime: expect.any(Date), localDateTime: expect.any(Date),
type: AssetType.IMAGE, type: AssetType.IMAGE,
originalFileName: 'photo.jpg', originalFileName: 'photo.jpg',
sidecarPath: null,
isExternal: true, isExternal: true,
}, },
], ],
@@ -423,57 +422,9 @@ describe(LibraryService.name, () => {
expect(jobMock.queue.mock.calls).toEqual([ expect(jobMock.queue.mock.calls).toEqual([
[ [
{ {
name: JobName.METADATA_EXTRACTION, name: JobName.SIDECAR_DISCOVERY,
data: { data: {
id: assetStub.image.id, id: assetStub.image.id,
source: 'upload',
},
},
],
]);
});
it('should import a new asset with sidecar', async () => {
const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg',
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
storageMock.checkFileExists.mockResolvedValue(true);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([
[
{
ownerId: mockUser.id,
libraryId: libraryStub.externalLibrary1.id,
checksum: expect.any(Buffer),
originalPath: '/data/user1/photo.jpg',
deviceAssetId: expect.any(String),
deviceId: 'Library Import',
fileCreatedAt: expect.any(Date),
fileModifiedAt: expect.any(Date),
localDateTime: expect.any(Date),
type: AssetType.IMAGE,
originalFileName: 'photo.jpg',
sidecarPath: '/data/user1/photo.jpg.xmp',
isExternal: true,
},
],
]);
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.METADATA_EXTRACTION,
data: {
id: assetStub.image.id,
source: 'upload',
}, },
}, },
], ],
@@ -507,7 +458,6 @@ describe(LibraryService.name, () => {
localDateTime: expect.any(Date), localDateTime: expect.any(Date),
type: AssetType.VIDEO, type: AssetType.VIDEO,
originalFileName: 'video.mp4', originalFileName: 'video.mp4',
sidecarPath: null,
isExternal: true, isExternal: true,
}, },
], ],
@@ -516,10 +466,9 @@ describe(LibraryService.name, () => {
expect(jobMock.queue.mock.calls).toEqual([ expect(jobMock.queue.mock.calls).toEqual([
[ [
{ {
name: JobName.METADATA_EXTRACTION, name: JobName.SIDECAR_DISCOVERY,
data: { data: {
id: assetStub.image.id, id: assetStub.image.id,
source: 'upload',
}, },
}, },
], ],

View File

@@ -396,12 +396,6 @@ export class LibraryService extends BaseService {
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`); const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
// TODO: doesn't xmp replace the file extension? Will need investigation
let sidecarPath: string | null = null;
if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) {
sidecarPath = `${assetPath}.xmp`;
}
const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE; const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE;
const mtime = stat.mtime; const mtime = stat.mtime;
@@ -418,8 +412,6 @@ export class LibraryService extends BaseService {
localDateTime: mtime, localDateTime: mtime,
type: assetType, type: assetType,
originalFileName: parse(assetPath).base, originalFileName: parse(assetPath).base,
sidecarPath,
isExternal: true, isExternal: true,
}); });
@@ -431,7 +423,11 @@ export class LibraryService extends BaseService {
async queuePostSyncJobs(asset: AssetEntity) { async queuePostSyncJobs(asset: AssetEntity) {
this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`); this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`);
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); // We queue a sidecar discovery which, in turn, queues metadata extraction
await this.jobRepository.queue({
name: JobName.SIDECAR_DISCOVERY,
data: { id: asset.id },
});
} }
async queueScan(id: string) { async queueScan(id: string) {

View File

@@ -698,7 +698,7 @@ export class MetadataService extends BaseService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!isSync && (!asset.isVisible || asset.sidecarPath)) { if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
@@ -720,6 +720,13 @@ export class MetadataService extends BaseService {
sidecarPath = sidecarPathWithoutExt; sidecarPath = sidecarPathWithoutExt;
} }
if (asset.isExternal) {
if (sidecarPath !== asset.sidecarPath) {
await this.assetRepository.update({ id: asset.id, sidecarPath });
}
return JobStatus.SUCCESS;
}
if (sidecarPath) { if (sidecarPath) {
await this.assetRepository.update({ id: asset.id, sidecarPath }); await this.assetRepository.update({ id: asset.id, sidecarPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;

View File

@@ -32,7 +32,7 @@ async function bootstrap() {
logger.setContext('Bootstrap'); logger.setContext('Bootstrap');
app.useLogger(logger); app.useLogger(logger);
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal', ...network.trustedProxies]); app.set('trust proxy', ['loopback', ...network.trustedProxies]);
app.set('etag', 'strong'); app.set('etag', 'strong');
app.use(cookieParser()); app.use(cookieParser());
app.use(json({ limit: '10mb' })); app.use(json({ limit: '10mb' }));

231
web/package-lock.json generated
View File

@@ -37,7 +37,7 @@
"@socket.io/component-emitter": "^3.1.0", "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.5", "@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/enhanced-img": "^0.4.0", "@sveltejs/enhanced-img": "^0.4.0",
"@sveltejs/kit": "^2.7.2", "@sveltejs/kit": "^2.12.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.4", "@testing-library/svelte": "^5.2.4",
@@ -80,7 +80,7 @@
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.9.0", "@types/node": "^22.10.2",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -1709,9 +1709,9 @@
"dev": true "dev": true
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
"integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1723,9 +1723,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
"integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1737,9 +1737,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
"integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1751,9 +1751,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
"integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1764,10 +1764,38 @@
"darwin" "darwin"
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
"integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
"integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
"integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1779,9 +1807,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
"integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1793,9 +1821,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
"integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1807,9 +1835,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
"integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1820,10 +1848,24 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
"integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
"integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -1835,9 +1877,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
"integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -1849,9 +1891,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
"integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -1863,9 +1905,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
"integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1877,9 +1919,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
"integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1891,9 +1933,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
"integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1905,9 +1947,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
"integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1919,9 +1961,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
"integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1965,9 +2007,9 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.8.3", "version": "2.13.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.3.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.13.0.tgz",
"integrity": "sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==", "integrity": "sha512-6t6ne00vZx/TjD6s0Jvwt8wRLKBwbSAN1nhlOzcLUSTYX1hTp4eCBaTPB5Yz/lu+tYcvz4YPEEuPv3yfsNp2gw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -1975,7 +2017,7 @@
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"devalue": "^5.1.0", "devalue": "^5.1.0",
"esm-env": "^1.0.0", "esm-env": "^1.2.1",
"import-meta-resolve": "^4.1.0", "import-meta-resolve": "^4.1.0",
"kleur": "^4.1.5", "kleur": "^4.1.5",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
@@ -1992,9 +2034,9 @@
"node": ">=18.13" "node": ">=18.13"
}, },
"peerDependencies": { "peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3" "vite": "^5.0.3 || ^6.0.0"
} }
}, },
"node_modules/@sveltejs/vite-plugin-svelte": { "node_modules/@sveltejs/vite-plugin-svelte": {
@@ -2285,9 +2327,10 @@
"dev": true "dev": true
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
}, },
"node_modules/@types/geojson": { "node_modules/@types/geojson": {
"version": "7946.0.14", "version": "7946.0.14",
@@ -4047,13 +4090,6 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/eslint/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/ansi-styles": { "node_modules/eslint/node_modules/ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -4192,9 +4228,10 @@
} }
}, },
"node_modules/esm-env": { "node_modules/esm-env": {
"version": "1.0.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
"license": "MIT"
}, },
"node_modules/esniff": { "node_modules/esniff": {
"version": "2.0.1", "version": "2.0.1",
@@ -5058,12 +5095,6 @@
"@types/estree": "^1.0.6" "@types/estree": "^1.0.6"
} }
}, },
"node_modules/is-reference/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/is-wsl": { "node_modules/is-wsl": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -5719,9 +5750,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -5729,6 +5760,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@@ -6643,13 +6675,13 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.21.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
"integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.6"
}, },
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@@ -6659,22 +6691,25 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.21.1", "@rollup/rollup-android-arm-eabi": "4.28.1",
"@rollup/rollup-android-arm64": "4.21.1", "@rollup/rollup-android-arm64": "4.28.1",
"@rollup/rollup-darwin-arm64": "4.21.1", "@rollup/rollup-darwin-arm64": "4.28.1",
"@rollup/rollup-darwin-x64": "4.21.1", "@rollup/rollup-darwin-x64": "4.28.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.21.1", "@rollup/rollup-freebsd-arm64": "4.28.1",
"@rollup/rollup-linux-arm-musleabihf": "4.21.1", "@rollup/rollup-freebsd-x64": "4.28.1",
"@rollup/rollup-linux-arm64-gnu": "4.21.1", "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
"@rollup/rollup-linux-arm64-musl": "4.21.1", "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", "@rollup/rollup-linux-arm64-gnu": "4.28.1",
"@rollup/rollup-linux-riscv64-gnu": "4.21.1", "@rollup/rollup-linux-arm64-musl": "4.28.1",
"@rollup/rollup-linux-s390x-gnu": "4.21.1", "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
"@rollup/rollup-linux-x64-gnu": "4.21.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
"@rollup/rollup-linux-x64-musl": "4.21.1", "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
"@rollup/rollup-win32-arm64-msvc": "4.21.1", "@rollup/rollup-linux-s390x-gnu": "4.28.1",
"@rollup/rollup-win32-ia32-msvc": "4.21.1", "@rollup/rollup-linux-x64-gnu": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.21.1", "@rollup/rollup-linux-x64-musl": "4.28.1",
"@rollup/rollup-win32-arm64-msvc": "4.28.1",
"@rollup/rollup-win32-ia32-msvc": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.28.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },

View File

@@ -29,7 +29,7 @@
"@socket.io/component-emitter": "^3.1.0", "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.5", "@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/enhanced-img": "^0.4.0", "@sveltejs/enhanced-img": "^0.4.0",
"@sveltejs/kit": "^2.7.2", "@sveltejs/kit": "^2.12.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.4", "@testing-library/svelte": "^5.2.4",

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
import { updatePerson, type AssetResponseDto, type PersonResponseDto } from '@immich/sdk';
import { mdiFaceManProfile } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
asset: AssetResponseDto;
person: PersonResponseDto;
}
let { asset, person }: Props = $props();
const handleSelectFeaturePhoto = async () => {
try {
await updatePerson({ id: person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
} catch (error) {
handleError(error, $t('errors.unable_to_set_feature_photo'));
}
};
</script>
<MenuOption text={$t('set_as_featured_photo')} icon={mdiFaceManProfile} onClick={handleSelectFeaturePhoto} />

View File

@@ -9,6 +9,7 @@
import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte'; import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte';
import RestoreAction from '$lib/components/asset-viewer/actions/restore-action.svelte'; import RestoreAction from '$lib/components/asset-viewer/actions/restore-action.svelte';
import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte'; import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte';
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte';
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte'; import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte'; import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte'; import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
@@ -27,6 +28,7 @@
AssetTypeEnum, AssetTypeEnum,
type AlbumResponseDto, type AlbumResponseDto,
type AssetResponseDto, type AssetResponseDto,
type PersonResponseDto,
type StackResponseDto, type StackResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { import {
@@ -50,6 +52,7 @@
interface Props { interface Props {
asset: AssetResponseDto; asset: AssetResponseDto;
album?: AlbumResponseDto | null; album?: AlbumResponseDto | null;
person?: PersonResponseDto | null;
stack?: StackResponseDto | null; stack?: StackResponseDto | null;
showDetailButton: boolean; showDetailButton: boolean;
showSlideshow?: boolean; showSlideshow?: boolean;
@@ -67,6 +70,7 @@
let { let {
asset, asset,
album = null, album = null,
person = null,
stack = null, stack = null,
showDetailButton, showDetailButton,
showSlideshow = false, showSlideshow = false,
@@ -169,6 +173,9 @@
{#if album} {#if album}
<SetAlbumCoverAction {asset} {album} /> <SetAlbumCoverAction {asset} {album} />
{/if} {/if}
{#if person}
<SetFeaturedPhotoAction {asset} {person} />
{/if}
{#if asset.type === AssetTypeEnum.Image} {#if asset.type === AssetTypeEnum.Image}
<SetProfilePictureAction {asset} /> <SetProfilePictureAction {asset} />
{/if} {/if}

View File

@@ -30,6 +30,7 @@
type ActivityResponseDto, type ActivityResponseDto,
type AlbumResponseDto, type AlbumResponseDto,
type AssetResponseDto, type AssetResponseDto,
type PersonResponseDto,
type StackResponseDto, type StackResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { onDestroy, onMount, untrack } from 'svelte'; import { onDestroy, onMount, untrack } from 'svelte';
@@ -56,6 +57,7 @@
withStacked?: boolean; withStacked?: boolean;
isShared?: boolean; isShared?: boolean;
album?: AlbumResponseDto | null; album?: AlbumResponseDto | null;
person?: PersonResponseDto | null;
onAction?: OnAction | undefined; onAction?: OnAction | undefined;
reactions?: ActivityResponseDto[]; reactions?: ActivityResponseDto[];
onClose: (dto: { asset: AssetResponseDto }) => void; onClose: (dto: { asset: AssetResponseDto }) => void;
@@ -72,6 +74,7 @@
withStacked = false, withStacked = false,
isShared = false, isShared = false,
album = null, album = null,
person = null,
onAction = undefined, onAction = undefined,
reactions = $bindable([]), reactions = $bindable([]),
onClose, onClose,
@@ -429,6 +432,7 @@
<AssetViewerNavBar <AssetViewerNavBar
{asset} {asset}
{album} {album}
{person}
{stack} {stack}
showDetailButton={enableDetailPanel} showDetailButton={enableDetailPanel}
showSlideshow={!!assetStore} showSlideshow={!!assetStore}

View File

@@ -1,5 +1,5 @@
<script lang="ts" module> <script lang="ts" module>
export type Color = 'transparent' | 'light' | 'dark' | 'red' | 'gray' | 'primary' | 'opaque' | 'alert'; export type Color = 'transparent' | 'light' | 'dark' | 'red' | 'gray' | 'primary' | 'opaque' | 'alert' | 'neutral';
export type Padding = '1' | '2' | '3'; export type Padding = '1' | '2' | '3';
</script> </script>
@@ -68,6 +68,8 @@
dark: 'bg-[#202123] hover:bg-[#d3d3d3]', dark: 'bg-[#202123] hover:bg-[#d3d3d3]',
alert: 'text-[#ff0000] hover:text-white', alert: 'text-[#ff0000] hover:text-white',
gray: 'bg-[#d3d3d3] hover:bg-[#e2e7e9] text-immich-dark-gray hover:text-black', gray: 'bg-[#d3d3d3] hover:bg-[#e2e7e9] text-immich-dark-gray hover:text-black',
neutral:
'dark:bg-immich-dark-gray dark:text-gray-300 hover:dark:bg-immich-dark-gray/50 hover:dark:text-gray-300 bg-gray-200 text-gray-700 hover:bg-gray-300',
primary: primary:
'bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 hover:dark:bg-immich-dark-primary/80 text-white dark:text-immich-dark-gray', 'bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 hover:dark:bg-immich-dark-primary/80 text-white dark:text-immich-dark-gray',
}; };

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/state';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants'; import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@@ -34,15 +34,17 @@
let hasSelection = $derived(selectedPeople.length > 0); let hasSelection = $derived(selectedPeople.length > 0);
let peopleToNotShow = $derived([...selectedPeople, person]); let peopleToNotShow = $derived([...selectedPeople, person]);
onMount(async () => { const handleSearch = async (sortFaces: boolean = false) => {
const data = await getAllPeople({ withHidden: false, closestPersonId: person.id }); const data = await getAllPeople({ withHidden: false, closestPersonId: sortFaces ? person.id : undefined });
people = data.people; people = data.people;
}); };
onMount(handleSearch);
const handleSwapPeople = async () => { const handleSwapPeople = async () => {
[person, selectedPeople[0]] = [selectedPeople[0], person]; [person, selectedPeople[0]] = [selectedPeople[0], person];
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE); page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`); await goto(`${AppRoute.PEOPLE}/${person.id}?${page.url.searchParams.toString()}`);
}; };
const onSelect = async (selected: PersonResponseDto) => { const onSelect = async (selected: PersonResponseDto) => {
@@ -149,8 +151,7 @@
<FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} /> <FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} />
</div> </div>
</div> </div>
<PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} {handleSearch} />
<PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} />
</section> </section>
</section> </section>
</section> </section>

View File

@@ -3,18 +3,20 @@
import FaceThumbnail from './face-thumbnail.svelte'; import FaceThumbnail from './face-thumbnail.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte'; import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { mdiSwapVertical } from '@mdi/js';
interface Props { interface Props {
screenHeight: number; screenHeight: number;
people: PersonResponseDto[]; people: PersonResponseDto[];
peopleToNotShow: PersonResponseDto[]; peopleToNotShow: PersonResponseDto[];
onSelect: (person: PersonResponseDto) => void; onSelect: (person: PersonResponseDto) => void;
handleSearch?: (sortFaces: boolean) => void;
} }
let { screenHeight, people, peopleToNotShow, onSelect }: Props = $props(); let { screenHeight, people, peopleToNotShow, onSelect, handleSearch }: Props = $props();
let searchedPeopleLocal: PersonResponseDto[] = $state([]); let searchedPeopleLocal: PersonResponseDto[] = $state([]);
let sortBySimilarirty = $state(false);
let name = $state(''); let name = $state('');
const showPeople = $derived( const showPeople = $derived(
@@ -24,12 +26,26 @@
); );
</script> </script>
<div class=" w-40 sm:w-48 md:w-96 h-14 mb-8"> <div class="w-40 sm:w-48 md:w-full h-14 flex gap-4 place-items-center">
<SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal /> <div class="md:w-96">
<SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
</div>
{#if handleSearch}
<CircleIconButton
icon={mdiSwapVertical}
onclick={() => {
sortBySimilarirty = !sortBySimilarirty;
handleSearch(sortBySimilarirty);
}}
color="neutral"
title={$t('sort_people_by_similarity')}
></CircleIconButton>
{/if}
</div> </div>
<div <div
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray" class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray mt-6"
style:max-height={screenHeight - 400 + 'px'} style:max-height={screenHeight - 400 + 'px'}
> >
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10"> <div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">

View File

@@ -5,10 +5,8 @@
import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte'; import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getAllTags, type TagResponseDto } from '@immich/sdk'; import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { AppRoute } from '$lib/constants';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
interface Props { interface Props {
@@ -22,6 +20,7 @@
let tagMap = $derived(Object.fromEntries(allTags.map((tag) => [tag.id, tag]))); let tagMap = $derived(Object.fromEntries(allTags.map((tag) => [tag.id, tag])));
let selectedIds = $state(new SvelteSet<string>()); let selectedIds = $state(new SvelteSet<string>());
let disabled = $derived(selectedIds.size === 0); let disabled = $derived(selectedIds.size === 0);
let allowCreate: boolean = $state(true);
onMount(async () => { onMount(async () => {
allTags = await getAllTags(); allTags = await getAllTags();
@@ -29,12 +28,18 @@
const handleSubmit = () => onTag([...selectedIds]); const handleSubmit = () => onTag([...selectedIds]);
const handleSelect = (option?: ComboBoxOption) => { const handleSelect = async (option?: ComboBoxOption) => {
if (!option) { if (!option) {
return; return;
} }
selectedIds.add(option.value); if (option.id) {
selectedIds.add(option.value);
} else {
const [newTag] = await upsertTags({ tagUpsertDto: { tags: [option.label] } });
allTags.push(newTag);
selectedIds.add(newTag.id);
}
}; };
const handleRemove = (tag: string) => { const handleRemove = (tag: string) => {
@@ -48,22 +53,13 @@
</script> </script>
<FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}> <FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
<div class="text-sm">
<p>
<FormatMessage key="tag_not_found_question">
{#snippet children({ message })}
<a href={AppRoute.TAGS} class="text-immich-primary dark:text-immich-dark-primary underline">
{message}
</a>
{/snippet}
</FormatMessage>
</p>
</div>
<form {onsubmit} autocomplete="off" id="create-tag-form"> <form {onsubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2"> <div class="my-4 flex flex-col gap-2">
<Combobox <Combobox
onSelect={handleSelect} onSelect={handleSelect}
label={$t('tag')} label={$t('tag')}
{allowCreate}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')} placeholder={$t('search_tags')}
/> />

View File

@@ -19,7 +19,7 @@
type ScrollTargetListener, type ScrollTargetListener,
} from '$lib/utils/timeline-util'; } from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables'; import { TUNABLES } from '$lib/utils/tunables';
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto } from '@immich/sdk';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
import { onDestroy, onMount, type Snippet } from 'svelte'; import { onDestroy, onMount, type Snippet } from 'svelte';
import Portal from '../shared-components/portal/portal.svelte'; import Portal from '../shared-components/portal/portal.svelte';
@@ -52,6 +52,7 @@
showArchiveIcon?: boolean; showArchiveIcon?: boolean;
isShared?: boolean; isShared?: boolean;
album?: AlbumResponseDto | null; album?: AlbumResponseDto | null;
person?: PersonResponseDto | null;
isShowDeleteConfirmation?: boolean; isShowDeleteConfirmation?: boolean;
onSelect?: (asset: AssetResponseDto) => void; onSelect?: (asset: AssetResponseDto) => void;
onEscape?: () => void; onEscape?: () => void;
@@ -70,6 +71,7 @@
showArchiveIcon = false, showArchiveIcon = false,
isShared = false, isShared = false,
album = null, album = null,
person = null,
isShowDeleteConfirmation = $bindable(false), isShowDeleteConfirmation = $bindable(false),
onSelect = () => {}, onSelect = () => {},
onEscape = () => {}, onEscape = () => {},
@@ -914,6 +916,7 @@
preloadAssets={$preloadAssets} preloadAssets={$preloadAssets}
{isShared} {isShared}
{album} {album}
{person}
onAction={handleAction} onAction={handleAction}
onPrevious={handlePrevious} onPrevious={handlePrevious}
onNext={handleNext} onNext={handleNext}

View File

@@ -36,6 +36,14 @@
options?: ComboBoxOption[]; options?: ComboBoxOption[];
selectedOption?: ComboBoxOption | undefined; selectedOption?: ComboBoxOption | undefined;
placeholder?: string; placeholder?: string;
/**
* whether creating new items is allowed.
*/
allowCreate?: boolean;
/**
* select first matching option on enter key.
*/
defaultFirstOption?: boolean;
onSelect?: (option: ComboBoxOption | undefined) => void; onSelect?: (option: ComboBoxOption | undefined) => void;
} }
@@ -45,6 +53,8 @@
options = [], options = [],
selectedOption = $bindable(), selectedOption = $bindable(),
placeholder = '', placeholder = '',
allowCreate = false,
defaultFirstOption = false,
onSelect = () => {}, onSelect = () => {},
}: Props = $props(); }: Props = $props();
@@ -141,7 +151,7 @@
const onInput: FormEventHandler<HTMLInputElement> = (event) => { const onInput: FormEventHandler<HTMLInputElement> = (event) => {
openDropdown(); openDropdown();
searchQuery = event.currentTarget.value; searchQuery = event.currentTarget.value;
selectedIndex = undefined; selectedIndex = defaultFirstOption ? 0 : undefined;
optionRefs[0]?.scrollIntoView({ block: 'nearest' }); optionRefs[0]?.scrollIntoView({ block: 'nearest' });
}; };
@@ -221,9 +231,15 @@
searchQuery = selectedOption ? selectedOption.label : ''; searchQuery = selectedOption ? selectedOption.label : '';
}); });
let filteredOptions = $derived( let filteredOptions = $derived.by(() => {
options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())), const _options = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
);
if (allowCreate && searchQuery !== '' && _options.filter((option) => option.label === searchQuery).length === 0) {
_options.unshift({ label: searchQuery, value: searchQuery });
}
return _options;
});
let position = $derived(calculatePosition(bounds)); let position = $derived(calculatePosition(bounds));
let dropdownDirection: 'bottom' | 'top' = $derived(getComboboxDirection(bounds, visualViewport)); let dropdownDirection: 'bottom' | 'top' = $derived(getComboboxDirection(bounds, visualViewport));
</script> </script>
@@ -352,7 +368,7 @@
id={`${listboxId}-${0}`} id={`${listboxId}-${0}`}
onclick={() => closeDropdown()} onclick={() => closeDropdown()}
> >
{$t('no_results')} {allowCreate ? searchQuery : $t('no_results')}
</li> </li>
{/if} {/if}
{#each filteredOptions as option, index (option.id || option.label)} {#each filteredOptions as option, index (option.id || option.label)}

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/state';
import { shouldIgnoreEvent } from '$lib/actions/shortcut'; import { shouldIgnoreEvent } from '$lib/actions/shortcut';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler } from '$lib/utils/file-uploader'; import { fileUploadHandler } from '$lib/utils/file-uploader';
@@ -8,8 +8,8 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import ImmichLogo from './immich-logo.svelte'; import ImmichLogo from './immich-logo.svelte';
let albumId = $derived(isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined); let albumId = $derived(isAlbumsRoute(page.route?.id) ? page.params.albumId : undefined);
let isShare = $derived(isSharedLinkRoute($page.route?.id)); let isShare = $derived(isSharedLinkRoute(page.route?.id));
let dragStartTarget: EventTarget | null = $state(null); let dragStartTarget: EventTarget | null = $state(null);

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/state';
import { focusTrap } from '$lib/actions/focus-trap'; import { focusTrap } from '$lib/actions/focus-trap';
import Button from '$lib/components/elements/buttons/button.svelte'; import Button from '$lib/components/elements/buttons/button.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
@@ -90,7 +90,7 @@
size="sm" size="sm"
shadow={false} shadow={false}
border border
aria-current={$page.url.pathname.includes('/admin') ? 'page' : undefined} aria-current={page.url.pathname.includes('/admin') ? 'page' : undefined}
> >
<div class="flex place-content-center place-items-center text-center gap-2 px-2"> <div class="flex place-content-center place-items-center text-center gap-2 px-2">
<Icon path={mdiWrench} size="18" ariaHidden /> <Icon path={mdiWrench} size="18" ariaHidden />

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/state';
import { clickOutside } from '$lib/actions/click-outside'; import { clickOutside } from '$lib/actions/click-outside';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
@@ -96,7 +96,7 @@
/> />
</div> </div>
{#if !$page.url.pathname.includes('/admin') && showUploadButton} {#if !page.url.pathname.includes('/admin') && showUploadButton}
<LinkButton onclick={onUploadClick} class="hidden lg:block"> <LinkButton onclick={onUploadClick} class="hidden lg:block">
<div class="flex gap-2"> <div class="flex gap-2">
<Icon path={mdiTrayArrowUp} size="1.5em" /> <Icon path={mdiTrayArrowUp} size="1.5em" />

View File

@@ -8,13 +8,13 @@
<script lang="ts"> <script lang="ts">
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
import { createContext } from '$lib/utils/context'; import { createContext } from '$lib/utils/context';
import { page } from '$app/stores'; import { page } from '$app/state';
import { handlePromiseError } from '$lib/utils';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { handlePromiseError } from '$lib/utils';
const getParamValues = (param: string) => { const getParamValues = (param: string) => {
return new Set(($page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== '')); return new Set((page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== ''));
}; };
interface Props { interface Props {
@@ -26,17 +26,16 @@
let { queryParam, state = writable(getParamValues(queryParam)), children }: Props = $props(); let { queryParam, state = writable(getParamValues(queryParam)), children }: Props = $props();
setAccordionState(state); setAccordionState(state);
$effect(() => { const searchParams = new URLSearchParams(page.url.searchParams);
if (queryParam && $state) {
const searchParams = new URLSearchParams($page.url.searchParams);
if ($state.size > 0) {
searchParams.set(queryParam, [...$state].join(' '));
} else {
searchParams.delete(queryParam);
}
handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true })); $effect(() => {
if ($state.size > 0) {
searchParams.set(queryParam, [...$state].join(' '));
} else {
searchParams.delete(queryParam);
} }
handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }));
}); });
</script> </script>

View File

@@ -2,7 +2,7 @@
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { mdiChevronDown, mdiChevronLeft } from '@mdi/js'; import { mdiChevronDown, mdiChevronLeft } from '@mdi/js';
import { resolveRoute } from '$app/paths'; import { resolveRoute } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/state';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -32,7 +32,7 @@
let routePath = $derived(resolveRoute(routeId, {})); let routePath = $derived(resolveRoute(routeId, {}));
$effect(() => { $effect(() => {
isSelected = ($page.route.id?.match(/^\/(admin|\(user\))\/[^/]*/) || [])[0] === routeId; isSelected = (page.route.id?.match(/^\/(admin|\(user\))\/[^/]*/) || [])[0] === routeId;
}); });
</script> </script>

View File

@@ -17,4 +17,14 @@
</span> </span>
{$t('review_duplicates')} {$t('review_duplicates')}
</a> </a>
<a
href={AppRoute.WORKFLOWS}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span
><Icon path={mdiContentDuplicate} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
</span>
{$t('workflows')}
</a>
</div> </div>

View File

@@ -46,6 +46,7 @@ export enum AppRoute {
UTILITIES = '/utilities', UTILITIES = '/utilities',
DUPLICATES = '/utilities/duplicates', DUPLICATES = '/utilities/duplicates',
WORKFLOWS = '/utilities/workflows',
FOLDERS = '/folders', FOLDERS = '/folders',
TAGS = '/tags', TAGS = '/tags',

View File

@@ -398,7 +398,9 @@ export class AssetStore {
} }
async updateOptions(options: AssetStoreOptions) { async updateOptions(options: AssetStoreOptions) {
if (!this.initialized) { // Make sure to re-initialize if the personId changes
const needsReinitializing = this.options.personId !== options.personId;
if (!this.initialized && !needsReinitializing) {
this.setOptions(options); this.setOptions(options);
return; return;
} }

View File

@@ -74,8 +74,13 @@
const assetStore = new AssetStore(assetStoreOptions); const assetStore = new AssetStore(assetStoreOptions);
$effect(() => { $effect(() => {
// Check to trigger rebuild the timeline when navigating between people from the info panel
const change = assetStoreOptions.personId !== data.person.id;
assetStoreOptions.personId = data.person.id; assetStoreOptions.personId = data.person.id;
handlePromiseError(assetStore.updateOptions(assetStoreOptions)); handlePromiseError(assetStore.updateOptions(assetStoreOptions));
if (change) {
assetStore.triggerUpdate();
}
}); });
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
@@ -454,6 +459,7 @@
{#key person.id} {#key person.id}
<AssetGrid <AssetGrid
enableRouting={true} enableRouting={true}
{person}
{assetStore} {assetStore}
{assetInteraction} {assetInteraction}
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON} isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/state';
</script> </script>
<svelte:head> <svelte:head>
@@ -8,7 +8,7 @@
<section class="flex flex-col px-4 h-screen w-screen place-content-center place-items-center"> <section class="flex flex-col px-4 h-screen w-screen place-content-center place-items-center">
<h1 class="py-10 text-4xl text-immich-primary dark:text-immich-dark-primary">Page not found :/</h1> <h1 class="py-10 text-4xl text-immich-primary dark:text-immich-dark-primary">Page not found :/</h1>
{#if $page.error?.message} {#if page.error?.message}
<h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{$page.error.message}</h2> <h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
{/if} {/if}
</section> </section>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<UserPageLayout title={data.meta.title}>
<div class="w-full h-full bg-gray-50 dark:bg-immich-dark-gray rounded-xl p-6">hello</div>
</UserPageLayout>

View File

@@ -0,0 +1,14 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const $t = await getFormatter();
return {
meta: {
title: $t('workflows'),
},
};
}) satisfies PageLoad;

View File

@@ -1,6 +1,6 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/state';
import Error from '$lib/components/error.svelte'; import Error from '$lib/components/error.svelte';
</script> </script>
<Error error={$page.error}></Error> <Error error={page.error}></Error>

View File

@@ -2,7 +2,7 @@
import { run } from 'svelte/legacy'; import { run } from 'svelte/legacy';
import { afterNavigate, beforeNavigate } from '$app/navigation'; import { afterNavigate, beforeNavigate } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/state';
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte'; import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
import AppleHeader from '$lib/components/shared-components/apple-header.svelte'; import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte'; import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
@@ -52,7 +52,7 @@
}; };
const getMyImmichLink = () => { const getMyImmichLink = () => {
return new URL($page.url.pathname + $page.url.search, 'https://my.immich.app'); return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
}; };
onMount(() => { onMount(() => {
@@ -66,8 +66,8 @@
document.removeEventListener('change', handleChangeTheme); document.removeEventListener('change', handleChangeTheme);
}); });
if (isSharedLinkRoute($page.route?.id)) { if (isSharedLinkRoute(page.route?.id)) {
setKey($page.params.key); setKey(page.params.key);
} }
beforeNavigate(({ from, to }) => { beforeNavigate(({ from, to }) => {
@@ -95,33 +95,33 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$page.data.meta?.title || 'Web'} - Immich</title> <title>{page.data.meta?.title || 'Web'} - Immich</title>
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" /> <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<meta name="theme-color" content="currentColor" /> <meta name="theme-color" content="currentColor" />
<AppleHeader /> <AppleHeader />
{#if $page.data.meta} {#if page.data.meta}
<meta name="description" content={$page.data.meta.description} /> <meta name="description" content={page.data.meta.description} />
<!-- Facebook Meta Tags --> <!-- Facebook Meta Tags -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content={$page.data.meta.title} /> <meta property="og:title" content={page.data.meta.title} />
<meta property="og:description" content={$page.data.meta.description} /> <meta property="og:description" content={page.data.meta.description} />
{#if $page.data.meta.imageUrl} {#if page.data.meta.imageUrl}
<meta <meta
property="og:image" property="og:image"
content={new URL($page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href} content={new URL(page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href}
/> />
{/if} {/if}
<!-- Twitter Meta Tags --> <!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={$page.data.meta.title} /> <meta name="twitter:title" content={page.data.meta.title} />
<meta name="twitter:description" content={$page.data.meta.description} /> <meta name="twitter:description" content={page.data.meta.description} />
{#if $page.data.meta.imageUrl} {#if page.data.meta.imageUrl}
<meta <meta
name="twitter:image" name="twitter:image"
content={new URL($page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href} content={new URL(page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href}
/> />
{/if} {/if}
{/if} {/if}
@@ -142,8 +142,8 @@
}} }}
/> />
{#if $page.data.error} {#if page.data.error}
<Error error={$page.data.error}></Error> <Error error={page.data.error}></Error>
{:else} {:else}
{@render children?.()} {@render children?.()}
{/if} {/if}