Compare commits
12 Commits
feat/ignor
...
feat/job-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ec2bd525 | ||
|
|
c5360e78c5 | ||
|
|
23014c263b | ||
|
|
2e5007adef | ||
|
|
c4531fc4d3 | ||
|
|
252d3f5f2c | ||
|
|
ef6c2bf547 | ||
|
|
6aad9fae8e | ||
|
|
45f7401513 | ||
|
|
3c7edba388 | ||
|
|
76a70703a5 | ||
|
|
f78066d4b9 |
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.50.0"
|
||||
constraints = "4.50.0"
|
||||
version = "4.52.0"
|
||||
constraints = "4.52.0"
|
||||
hashes = [
|
||||
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
|
||||
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
|
||||
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
|
||||
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
|
||||
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
|
||||
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
|
||||
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
|
||||
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
|
||||
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
|
||||
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
|
||||
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
|
||||
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
|
||||
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
|
||||
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
|
||||
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
|
||||
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
|
||||
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
|
||||
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
|
||||
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
|
||||
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
|
||||
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
|
||||
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
||||
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
||||
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
||||
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
||||
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
||||
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
||||
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
||||
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
||||
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
||||
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
||||
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
||||
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
||||
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
||||
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
||||
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
||||
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
||||
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
||||
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
||||
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
||||
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
||||
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
||||
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
|
||||
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
|
||||
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
|
||||
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
|
||||
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
|
||||
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
|
||||
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
|
||||
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
||||
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
||||
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
||||
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
||||
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
||||
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.50.0"
|
||||
version = "4.52.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.50.0"
|
||||
constraints = "4.50.0"
|
||||
version = "4.52.0"
|
||||
constraints = "4.52.0"
|
||||
hashes = [
|
||||
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
|
||||
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
|
||||
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
|
||||
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
|
||||
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
|
||||
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
|
||||
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
|
||||
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
|
||||
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
|
||||
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
|
||||
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
|
||||
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
|
||||
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
|
||||
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
|
||||
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
|
||||
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
|
||||
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
|
||||
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
|
||||
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
|
||||
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
|
||||
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
|
||||
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
||||
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
||||
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
||||
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
||||
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
||||
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
||||
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
||||
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
||||
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
||||
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
||||
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
||||
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
||||
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
||||
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
||||
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
||||
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
||||
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
||||
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
||||
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
||||
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
||||
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
||||
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
|
||||
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
|
||||
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
|
||||
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
|
||||
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
|
||||
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
|
||||
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
|
||||
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
||||
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
||||
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
||||
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
||||
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
||||
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.50.0"
|
||||
version = "4.52.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c
|
||||
image: grafana/grafana:11.5.0-ubuntu@sha256:3c9e2b202eb933a22da5f2b5a22c98a665493f603b452263d9d6f242a87f60d7
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
||||
|
||||
### Mobile app
|
||||
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x and FVM to be installed on your system.
|
||||
|
||||
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
|
||||
- ARM NN (Mali)
|
||||
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
|
||||
- OpenVINO (Intel discrete GPUs such as Iris Xe and Arc)
|
||||
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -43,8 +43,9 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
|
||||
#### OpenVINO
|
||||
|
||||
- The server must have a discrete GPU, i.e. Iris Xe or Arc. Expect issues when attempting to use integrated graphics.
|
||||
- Integrated GPUs are more likely to experience issues than discrete GPUs, especially for older processors or servers with low RAM.
|
||||
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
|
||||
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function VersionSwitcher(): JSX.Element {
|
||||
return (
|
||||
versions.length > 0 && (
|
||||
<DropdownNavbarItem
|
||||
className="navbar__item"
|
||||
className="version-switcher-34ab39"
|
||||
label={label}
|
||||
mobile={windowSize === 'mobile'}
|
||||
items={versions.map(({ label, url }) => ({
|
||||
|
||||
@@ -75,6 +75,11 @@ div[class^='announcementBar_'] {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* workaround for version switcher PR 15894 */
|
||||
div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ describe('/people', () => {
|
||||
.send({
|
||||
name: 'New Person',
|
||||
birthDate: '1990-01-01',
|
||||
color: '#333',
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
@@ -273,6 +274,24 @@ describe('/people', () => {
|
||||
expect(body).toMatchObject({ birthDate: null });
|
||||
});
|
||||
|
||||
it('should set a color', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/people/${visiblePerson.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ color: '#555' });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ color: '#555' });
|
||||
});
|
||||
|
||||
it('should clear a color', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/people/${visiblePerson.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ color: null });
|
||||
expect(status).toBe(200);
|
||||
expect(body.color).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should mark a person as favorite', async () => {
|
||||
const person = await utils.createPerson(admin.accessToken, {
|
||||
name: 'visible_person',
|
||||
|
||||
@@ -436,6 +436,7 @@
|
||||
"back_close_deselect": "Back, close, or deselect",
|
||||
"backward": "Backward",
|
||||
"birthdate_saved": "Date of birth saved successfully",
|
||||
"show_shared_links": "Show shared links",
|
||||
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
|
||||
"blurred_background": "Blurred background",
|
||||
"bugs_and_feature_requests": "Bugs & Feature Requests",
|
||||
@@ -768,8 +769,10 @@
|
||||
"go_to_search": "Go to search",
|
||||
"go_to_folder": "Go to folder",
|
||||
"group_albums_by": "Group albums by...",
|
||||
"group_country": "Group by country",
|
||||
"group_no": "No grouping",
|
||||
"group_owner": "Group by owner",
|
||||
"group_places_by": "Group places by...",
|
||||
"group_year": "Group by year",
|
||||
"has_quota": "Has quota",
|
||||
"hi_user": "Hi {name} ({email})",
|
||||
@@ -802,6 +805,7 @@
|
||||
"include_shared_albums": "Include shared albums",
|
||||
"include_shared_partner_assets": "Include shared partner assets",
|
||||
"individual_share": "Individual share",
|
||||
"individual_shares": "Individual shares",
|
||||
"info": "Info",
|
||||
"interval": {
|
||||
"day_at_onepm": "Every day at 1pm",
|
||||
@@ -987,6 +991,7 @@
|
||||
"pick_a_location": "Pick a location",
|
||||
"place": "Place",
|
||||
"places": "Places",
|
||||
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
|
||||
"play": "Play",
|
||||
"play_memories": "Play memories",
|
||||
"play_motion_photo": "Play Motion Photo",
|
||||
@@ -1169,6 +1174,7 @@
|
||||
"shared_from_partner": "Photos from {partner}",
|
||||
"shared_link_options": "Shared link options",
|
||||
"shared_links": "Shared links",
|
||||
"shared_links_description": "Share photos and videos with a link",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
|
||||
"shared_with_partner": "Shared with {partner}",
|
||||
"sharing": "Sharing",
|
||||
@@ -1278,6 +1284,7 @@
|
||||
"unfavorite": "Unfavorite",
|
||||
"unhide_person": "Unhide person",
|
||||
"unknown": "Unknown",
|
||||
"unknown_country": "Unknown Country",
|
||||
"unknown_year": "Unknown Year",
|
||||
"unlimited": "Unlimited",
|
||||
"unlink_motion_video": "Unlink motion video",
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -408,6 +408,8 @@ Class | Method | HTTP request | Description
|
||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||
- [SharedLinkType](doc//SharedLinkType.md)
|
||||
- [SharedLinksResponse](doc//SharedLinksResponse.md)
|
||||
- [SharedLinksUpdate](doc//SharedLinksUpdate.md)
|
||||
- [SignUpDto](doc//SignUpDto.md)
|
||||
- [SmartSearchDto](doc//SmartSearchDto.md)
|
||||
- [SourceType](doc//SourceType.md)
|
||||
|
||||
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@@ -221,6 +221,8 @@ part 'model/shared_link_create_dto.dart';
|
||||
part 'model/shared_link_edit_dto.dart';
|
||||
part 'model/shared_link_response_dto.dart';
|
||||
part 'model/shared_link_type.dart';
|
||||
part 'model/shared_links_response.dart';
|
||||
part 'model/shared_links_update.dart';
|
||||
part 'model/sign_up_dto.dart';
|
||||
part 'model/smart_search_dto.dart';
|
||||
part 'model/source_type.dart';
|
||||
|
||||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@@ -496,6 +496,10 @@ class ApiClient {
|
||||
return SharedLinkResponseDto.fromJson(value);
|
||||
case 'SharedLinkType':
|
||||
return SharedLinkTypeTypeTransformer().decode(value);
|
||||
case 'SharedLinksResponse':
|
||||
return SharedLinksResponse.fromJson(value);
|
||||
case 'SharedLinksUpdate':
|
||||
return SharedLinksUpdate.fromJson(value);
|
||||
case 'SignUpDto':
|
||||
return SignUpDto.fromJson(value);
|
||||
case 'SmartSearchDto':
|
||||
|
||||
13
mobile/openapi/lib/model/people_update_item.dart
generated
13
mobile/openapi/lib/model/people_update_item.dart
generated
@@ -14,6 +14,7 @@ class PeopleUpdateItem {
|
||||
/// Returns a new [PeopleUpdateItem] instance.
|
||||
PeopleUpdateItem({
|
||||
this.birthDate,
|
||||
this.color,
|
||||
this.featureFaceAssetId,
|
||||
required this.id,
|
||||
this.isFavorite,
|
||||
@@ -24,6 +25,8 @@ class PeopleUpdateItem {
|
||||
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
|
||||
DateTime? birthDate;
|
||||
|
||||
String? color;
|
||||
|
||||
/// Asset is used to get the feature face thumbnail.
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@@ -65,6 +68,7 @@ class PeopleUpdateItem {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem &&
|
||||
other.birthDate == birthDate &&
|
||||
other.color == color &&
|
||||
other.featureFaceAssetId == featureFaceAssetId &&
|
||||
other.id == id &&
|
||||
other.isFavorite == isFavorite &&
|
||||
@@ -75,6 +79,7 @@ class PeopleUpdateItem {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(color == null ? 0 : color!.hashCode) +
|
||||
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
@@ -82,7 +87,7 @@ class PeopleUpdateItem {
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
|
||||
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, color=$color, featureFaceAssetId=$featureFaceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -91,6 +96,11 @@ class PeopleUpdateItem {
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
if (this.color != null) {
|
||||
json[r'color'] = this.color;
|
||||
} else {
|
||||
// json[r'color'] = null;
|
||||
}
|
||||
if (this.featureFaceAssetId != null) {
|
||||
json[r'featureFaceAssetId'] = this.featureFaceAssetId;
|
||||
} else {
|
||||
@@ -125,6 +135,7 @@ class PeopleUpdateItem {
|
||||
|
||||
return PeopleUpdateItem(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
|
||||
13
mobile/openapi/lib/model/person_create_dto.dart
generated
13
mobile/openapi/lib/model/person_create_dto.dart
generated
@@ -14,6 +14,7 @@ class PersonCreateDto {
|
||||
/// Returns a new [PersonCreateDto] instance.
|
||||
PersonCreateDto({
|
||||
this.birthDate,
|
||||
this.color,
|
||||
this.isFavorite,
|
||||
this.isHidden,
|
||||
this.name,
|
||||
@@ -22,6 +23,8 @@ class PersonCreateDto {
|
||||
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
|
||||
DateTime? birthDate;
|
||||
|
||||
String? color;
|
||||
|
||||
///
|
||||
/// 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
|
||||
@@ -51,6 +54,7 @@ class PersonCreateDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonCreateDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.color == color &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name;
|
||||
@@ -59,12 +63,13 @@ class PersonCreateDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(color == null ? 0 : color!.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(isHidden == null ? 0 : isHidden!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonCreateDto[birthDate=$birthDate, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
|
||||
String toString() => 'PersonCreateDto[birthDate=$birthDate, color=$color, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -73,6 +78,11 @@ class PersonCreateDto {
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
if (this.color != null) {
|
||||
json[r'color'] = this.color;
|
||||
} else {
|
||||
// json[r'color'] = null;
|
||||
}
|
||||
if (this.isFavorite != null) {
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
} else {
|
||||
@@ -101,6 +111,7 @@ class PersonCreateDto {
|
||||
|
||||
return PersonCreateDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
|
||||
20
mobile/openapi/lib/model/person_response_dto.dart
generated
20
mobile/openapi/lib/model/person_response_dto.dart
generated
@@ -14,6 +14,7 @@ class PersonResponseDto {
|
||||
/// Returns a new [PersonResponseDto] instance.
|
||||
PersonResponseDto({
|
||||
required this.birthDate,
|
||||
this.color,
|
||||
required this.id,
|
||||
this.isFavorite,
|
||||
required this.isHidden,
|
||||
@@ -24,6 +25,15 @@ class PersonResponseDto {
|
||||
|
||||
DateTime? birthDate;
|
||||
|
||||
/// This property was added in v1.126.0
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? color;
|
||||
|
||||
String id;
|
||||
|
||||
/// This property was added in v1.126.0
|
||||
@@ -53,6 +63,7 @@ class PersonResponseDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.color == color &&
|
||||
other.id == id &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.isHidden == isHidden &&
|
||||
@@ -64,6 +75,7 @@ class PersonResponseDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(color == null ? 0 : color!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(isHidden.hashCode) +
|
||||
@@ -72,7 +84,7 @@ class PersonResponseDto {
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -80,6 +92,11 @@ class PersonResponseDto {
|
||||
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
if (this.color != null) {
|
||||
json[r'color'] = this.color;
|
||||
} else {
|
||||
// json[r'color'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
if (this.isFavorite != null) {
|
||||
@@ -108,6 +125,7 @@ class PersonResponseDto {
|
||||
|
||||
return PersonResponseDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||
|
||||
13
mobile/openapi/lib/model/person_update_dto.dart
generated
13
mobile/openapi/lib/model/person_update_dto.dart
generated
@@ -14,6 +14,7 @@ class PersonUpdateDto {
|
||||
/// Returns a new [PersonUpdateDto] instance.
|
||||
PersonUpdateDto({
|
||||
this.birthDate,
|
||||
this.color,
|
||||
this.featureFaceAssetId,
|
||||
this.isFavorite,
|
||||
this.isHidden,
|
||||
@@ -23,6 +24,8 @@ class PersonUpdateDto {
|
||||
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
|
||||
DateTime? birthDate;
|
||||
|
||||
String? color;
|
||||
|
||||
/// Asset is used to get the feature face thumbnail.
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@@ -61,6 +64,7 @@ class PersonUpdateDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.color == color &&
|
||||
other.featureFaceAssetId == featureFaceAssetId &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.isHidden == isHidden &&
|
||||
@@ -70,13 +74,14 @@ class PersonUpdateDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(color == null ? 0 : color!.hashCode) +
|
||||
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(isHidden == null ? 0 : isHidden!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
|
||||
String toString() => 'PersonUpdateDto[birthDate=$birthDate, color=$color, featureFaceAssetId=$featureFaceAssetId, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -85,6 +90,11 @@ class PersonUpdateDto {
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
if (this.color != null) {
|
||||
json[r'color'] = this.color;
|
||||
} else {
|
||||
// json[r'color'] = null;
|
||||
}
|
||||
if (this.featureFaceAssetId != null) {
|
||||
json[r'featureFaceAssetId'] = this.featureFaceAssetId;
|
||||
} else {
|
||||
@@ -118,6 +128,7 @@ class PersonUpdateDto {
|
||||
|
||||
return PersonUpdateDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden'),
|
||||
|
||||
@@ -14,6 +14,7 @@ class PersonWithFacesResponseDto {
|
||||
/// Returns a new [PersonWithFacesResponseDto] instance.
|
||||
PersonWithFacesResponseDto({
|
||||
required this.birthDate,
|
||||
this.color,
|
||||
this.faces = const [],
|
||||
required this.id,
|
||||
this.isFavorite,
|
||||
@@ -25,6 +26,15 @@ class PersonWithFacesResponseDto {
|
||||
|
||||
DateTime? birthDate;
|
||||
|
||||
/// This property was added in v1.126.0
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? color;
|
||||
|
||||
List<AssetFaceWithoutPersonResponseDto> faces;
|
||||
|
||||
String id;
|
||||
@@ -56,6 +66,7 @@ class PersonWithFacesResponseDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.color == color &&
|
||||
_deepEquality.equals(other.faces, faces) &&
|
||||
other.id == id &&
|
||||
other.isFavorite == isFavorite &&
|
||||
@@ -68,6 +79,7 @@ class PersonWithFacesResponseDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(color == null ? 0 : color!.hashCode) +
|
||||
(faces.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
@@ -77,7 +89,7 @@ class PersonWithFacesResponseDto {
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, color=$color, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -85,6 +97,11 @@ class PersonWithFacesResponseDto {
|
||||
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
if (this.color != null) {
|
||||
json[r'color'] = this.color;
|
||||
} else {
|
||||
// json[r'color'] = null;
|
||||
}
|
||||
json[r'faces'] = this.faces;
|
||||
json[r'id'] = this.id;
|
||||
@@ -114,6 +131,7 @@ class PersonWithFacesResponseDto {
|
||||
|
||||
return PersonWithFacesResponseDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||
color: mapValueOfType<String>(json, r'color'),
|
||||
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
|
||||
107
mobile/openapi/lib/model/shared_links_response.dart
generated
Normal file
107
mobile/openapi/lib/model/shared_links_response.dart
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SharedLinksResponse {
|
||||
/// Returns a new [SharedLinksResponse] instance.
|
||||
SharedLinksResponse({
|
||||
this.enabled = true,
|
||||
this.sidebarWeb = false,
|
||||
});
|
||||
|
||||
bool enabled;
|
||||
|
||||
bool sidebarWeb;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinksResponse &&
|
||||
other.enabled == enabled &&
|
||||
other.sidebarWeb == sidebarWeb;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(enabled.hashCode) +
|
||||
(sidebarWeb.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharedLinksResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'sidebarWeb'] = this.sidebarWeb;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SharedLinksResponse] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SharedLinksResponse? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SharedLinksResponse");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SharedLinksResponse(
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
sidebarWeb: mapValueOfType<bool>(json, r'sidebarWeb')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SharedLinksResponse> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SharedLinksResponse>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SharedLinksResponse.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SharedLinksResponse> mapFromJson(dynamic json) {
|
||||
final map = <String, SharedLinksResponse>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SharedLinksResponse.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SharedLinksResponse-objects as value to a dart map
|
||||
static Map<String, List<SharedLinksResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SharedLinksResponse>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SharedLinksResponse.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'enabled',
|
||||
'sidebarWeb',
|
||||
};
|
||||
}
|
||||
|
||||
125
mobile/openapi/lib/model/shared_links_update.dart
generated
Normal file
125
mobile/openapi/lib/model/shared_links_update.dart
generated
Normal file
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SharedLinksUpdate {
|
||||
/// Returns a new [SharedLinksUpdate] instance.
|
||||
SharedLinksUpdate({
|
||||
this.enabled,
|
||||
this.sidebarWeb,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? enabled;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? sidebarWeb;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinksUpdate &&
|
||||
other.enabled == enabled &&
|
||||
other.sidebarWeb == sidebarWeb;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(enabled == null ? 0 : enabled!.hashCode) +
|
||||
(sidebarWeb == null ? 0 : sidebarWeb!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharedLinksUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.enabled != null) {
|
||||
json[r'enabled'] = this.enabled;
|
||||
} else {
|
||||
// json[r'enabled'] = null;
|
||||
}
|
||||
if (this.sidebarWeb != null) {
|
||||
json[r'sidebarWeb'] = this.sidebarWeb;
|
||||
} else {
|
||||
// json[r'sidebarWeb'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SharedLinksUpdate] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SharedLinksUpdate? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SharedLinksUpdate");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SharedLinksUpdate(
|
||||
enabled: mapValueOfType<bool>(json, r'enabled'),
|
||||
sidebarWeb: mapValueOfType<bool>(json, r'sidebarWeb'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SharedLinksUpdate> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SharedLinksUpdate>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SharedLinksUpdate.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SharedLinksUpdate> mapFromJson(dynamic json) {
|
||||
final map = <String, SharedLinksUpdate>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SharedLinksUpdate.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SharedLinksUpdate-objects as value to a dart map
|
||||
static Map<String, List<SharedLinksUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SharedLinksUpdate>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SharedLinksUpdate.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class UserPreferencesResponseDto {
|
||||
required this.people,
|
||||
required this.purchase,
|
||||
required this.ratings,
|
||||
required this.sharedLinks,
|
||||
required this.tags,
|
||||
});
|
||||
|
||||
@@ -40,6 +41,8 @@ class UserPreferencesResponseDto {
|
||||
|
||||
RatingsResponse ratings;
|
||||
|
||||
SharedLinksResponse sharedLinks;
|
||||
|
||||
TagsResponse tags;
|
||||
|
||||
@override
|
||||
@@ -52,6 +55,7 @@ class UserPreferencesResponseDto {
|
||||
other.people == people &&
|
||||
other.purchase == purchase &&
|
||||
other.ratings == ratings &&
|
||||
other.sharedLinks == sharedLinks &&
|
||||
other.tags == tags;
|
||||
|
||||
@override
|
||||
@@ -65,10 +69,11 @@ class UserPreferencesResponseDto {
|
||||
(people.hashCode) +
|
||||
(purchase.hashCode) +
|
||||
(ratings.hashCode) +
|
||||
(sharedLinks.hashCode) +
|
||||
(tags.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]';
|
||||
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -80,6 +85,7 @@ class UserPreferencesResponseDto {
|
||||
json[r'people'] = this.people;
|
||||
json[r'purchase'] = this.purchase;
|
||||
json[r'ratings'] = this.ratings;
|
||||
json[r'sharedLinks'] = this.sharedLinks;
|
||||
json[r'tags'] = this.tags;
|
||||
return json;
|
||||
}
|
||||
@@ -101,6 +107,7 @@ class UserPreferencesResponseDto {
|
||||
people: PeopleResponse.fromJson(json[r'people'])!,
|
||||
purchase: PurchaseResponse.fromJson(json[r'purchase'])!,
|
||||
ratings: RatingsResponse.fromJson(json[r'ratings'])!,
|
||||
sharedLinks: SharedLinksResponse.fromJson(json[r'sharedLinks'])!,
|
||||
tags: TagsResponse.fromJson(json[r'tags'])!,
|
||||
);
|
||||
}
|
||||
@@ -157,6 +164,7 @@ class UserPreferencesResponseDto {
|
||||
'people',
|
||||
'purchase',
|
||||
'ratings',
|
||||
'sharedLinks',
|
||||
'tags',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class UserPreferencesUpdateDto {
|
||||
this.people,
|
||||
this.purchase,
|
||||
this.ratings,
|
||||
this.sharedLinks,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
@@ -88,6 +89,14 @@ class UserPreferencesUpdateDto {
|
||||
///
|
||||
RatingsUpdate? ratings;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
SharedLinksUpdate? sharedLinks;
|
||||
|
||||
///
|
||||
/// 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
|
||||
@@ -106,6 +115,7 @@ class UserPreferencesUpdateDto {
|
||||
other.people == people &&
|
||||
other.purchase == purchase &&
|
||||
other.ratings == ratings &&
|
||||
other.sharedLinks == sharedLinks &&
|
||||
other.tags == tags;
|
||||
|
||||
@override
|
||||
@@ -119,10 +129,11 @@ class UserPreferencesUpdateDto {
|
||||
(people == null ? 0 : people!.hashCode) +
|
||||
(purchase == null ? 0 : purchase!.hashCode) +
|
||||
(ratings == null ? 0 : ratings!.hashCode) +
|
||||
(sharedLinks == null ? 0 : sharedLinks!.hashCode) +
|
||||
(tags == null ? 0 : tags!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]';
|
||||
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -166,6 +177,11 @@ class UserPreferencesUpdateDto {
|
||||
} else {
|
||||
// json[r'ratings'] = null;
|
||||
}
|
||||
if (this.sharedLinks != null) {
|
||||
json[r'sharedLinks'] = this.sharedLinks;
|
||||
} else {
|
||||
// json[r'sharedLinks'] = null;
|
||||
}
|
||||
if (this.tags != null) {
|
||||
json[r'tags'] = this.tags;
|
||||
} else {
|
||||
@@ -191,6 +207,7 @@ class UserPreferencesUpdateDto {
|
||||
people: PeopleUpdate.fromJson(json[r'people']),
|
||||
purchase: PurchaseUpdate.fromJson(json[r'purchase']),
|
||||
ratings: RatingsUpdate.fromJson(json[r'ratings']),
|
||||
sharedLinks: SharedLinksUpdate.fromJson(json[r'sharedLinks']),
|
||||
tags: TagsUpdate.fromJson(json[r'tags']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10286,6 +10286,10 @@
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"featureFaceAssetId": {
|
||||
"description": "Asset is used to get the feature face thumbnail.",
|
||||
"type": "string"
|
||||
@@ -10402,6 +10406,10 @@
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -10423,6 +10431,10 @@
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
"description": "This property was added in v1.126.0",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10473,6 +10485,10 @@
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"featureFaceAssetId": {
|
||||
"description": "Asset is used to get the feature face thumbnail.",
|
||||
"type": "string"
|
||||
@@ -10498,6 +10514,10 @@
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
"description": "This property was added in v1.126.0",
|
||||
"type": "string"
|
||||
},
|
||||
"faces": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto"
|
||||
@@ -11494,6 +11514,34 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SharedLinksResponse": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarWeb": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"sidebarWeb"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SharedLinksUpdate": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sidebarWeb": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SignUpDto": {
|
||||
"properties": {
|
||||
"email": {
|
||||
@@ -12611,7 +12659,6 @@
|
||||
"properties": {
|
||||
"color": {
|
||||
"nullable": true,
|
||||
"pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -13141,6 +13188,9 @@
|
||||
"ratings": {
|
||||
"$ref": "#/components/schemas/RatingsResponse"
|
||||
},
|
||||
"sharedLinks": {
|
||||
"$ref": "#/components/schemas/SharedLinksResponse"
|
||||
},
|
||||
"tags": {
|
||||
"$ref": "#/components/schemas/TagsResponse"
|
||||
}
|
||||
@@ -13154,6 +13204,7 @@
|
||||
"people",
|
||||
"purchase",
|
||||
"ratings",
|
||||
"sharedLinks",
|
||||
"tags"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -13184,6 +13235,9 @@
|
||||
"ratings": {
|
||||
"$ref": "#/components/schemas/RatingsUpdate"
|
||||
},
|
||||
"sharedLinks": {
|
||||
"$ref": "#/components/schemas/SharedLinksUpdate"
|
||||
},
|
||||
"tags": {
|
||||
"$ref": "#/components/schemas/TagsUpdate"
|
||||
}
|
||||
|
||||
@@ -113,6 +113,10 @@ export type PurchaseResponse = {
|
||||
export type RatingsResponse = {
|
||||
enabled: boolean;
|
||||
};
|
||||
export type SharedLinksResponse = {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
};
|
||||
export type TagsResponse = {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
@@ -126,6 +130,7 @@ export type UserPreferencesResponseDto = {
|
||||
people: PeopleResponse;
|
||||
purchase: PurchaseResponse;
|
||||
ratings: RatingsResponse;
|
||||
sharedLinks: SharedLinksResponse;
|
||||
tags: TagsResponse;
|
||||
};
|
||||
export type AvatarUpdate = {
|
||||
@@ -158,6 +163,10 @@ export type PurchaseUpdate = {
|
||||
export type RatingsUpdate = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
export type SharedLinksUpdate = {
|
||||
enabled?: boolean;
|
||||
sidebarWeb?: boolean;
|
||||
};
|
||||
export type TagsUpdate = {
|
||||
enabled?: boolean;
|
||||
sidebarWeb?: boolean;
|
||||
@@ -171,6 +180,7 @@ export type UserPreferencesUpdateDto = {
|
||||
people?: PeopleUpdate;
|
||||
purchase?: PurchaseUpdate;
|
||||
ratings?: RatingsUpdate;
|
||||
sharedLinks?: SharedLinksUpdate;
|
||||
tags?: TagsUpdate;
|
||||
};
|
||||
export type AlbumUserResponseDto = {
|
||||
@@ -213,6 +223,8 @@ export type AssetFaceWithoutPersonResponseDto = {
|
||||
};
|
||||
export type PersonWithFacesResponseDto = {
|
||||
birthDate: string | null;
|
||||
/** This property was added in v1.126.0 */
|
||||
color?: string;
|
||||
faces: AssetFaceWithoutPersonResponseDto[];
|
||||
id: string;
|
||||
/** This property was added in v1.126.0 */
|
||||
@@ -493,6 +505,8 @@ export type DuplicateResponseDto = {
|
||||
};
|
||||
export type PersonResponseDto = {
|
||||
birthDate: string | null;
|
||||
/** This property was added in v1.126.0 */
|
||||
color?: string;
|
||||
id: string;
|
||||
/** This property was added in v1.126.0 */
|
||||
isFavorite?: boolean;
|
||||
@@ -693,6 +707,7 @@ export type PersonCreateDto = {
|
||||
/** Person date of birth.
|
||||
Note: the mobile app cannot currently set the birth date to null. */
|
||||
birthDate?: string | null;
|
||||
color?: string | null;
|
||||
isFavorite?: boolean;
|
||||
/** Person visibility */
|
||||
isHidden?: boolean;
|
||||
@@ -703,6 +718,7 @@ export type PeopleUpdateItem = {
|
||||
/** Person date of birth.
|
||||
Note: the mobile app cannot currently set the birth date to null. */
|
||||
birthDate?: string | null;
|
||||
color?: string | null;
|
||||
/** Asset is used to get the feature face thumbnail. */
|
||||
featureFaceAssetId?: string;
|
||||
/** Person id. */
|
||||
@@ -720,6 +736,7 @@ export type PersonUpdateDto = {
|
||||
/** Person date of birth.
|
||||
Note: the mobile app cannot currently set the birth date to null. */
|
||||
birthDate?: string | null;
|
||||
color?: string | null;
|
||||
/** Asset is used to get the feature face thumbnail. */
|
||||
featureFaceAssetId?: string;
|
||||
isFavorite?: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20250123@sha256:04eba5cd87d61bc3d20a3915b2302f04d08fbc329c55ee0cde103c502f59f412 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20250204@sha256:8b203f19f4d5cf4619b60ee5f50d6d4b5ea3745747f5e5170d1b7404ebeb0792 AS dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -42,7 +42,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20250123@sha256:591739983913f82672d8191258f3a1a24c123db0d619ff91fca8fef431ee1338
|
||||
FROM ghcr.io/immich-app/base-server-prod:20250204@sha256:2af3da713d5ab3ccca23b216b747557ea6016117e72deac101e35069ccaf9b5e
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
1872
server/package-lock.json
generated
1872
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,16 +35,16 @@
|
||||
"email:dev": "email dev -p 3050 --dir src/emails"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.0",
|
||||
"@nestjs/common": "^10.2.2",
|
||||
"@nestjs/core": "^10.2.2",
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
"@nestjs/common": "^11.0.4",
|
||||
"@nestjs/core": "^11.0.4",
|
||||
"@nestjs/event-emitter": "^3.0.0",
|
||||
"@nestjs/platform-express": "^10.2.2",
|
||||
"@nestjs/platform-socket.io": "^10.2.2",
|
||||
"@nestjs/platform-express": "^11.0.4",
|
||||
"@nestjs/platform-socket.io": "^11.0.4",
|
||||
"@nestjs/schedule": "^5.0.0",
|
||||
"@nestjs/swagger": "^8.0.0",
|
||||
"@nestjs/typeorm": "^10.0.0",
|
||||
"@nestjs/websockets": "^10.2.2",
|
||||
"@nestjs/swagger": "^11.0.2",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.0.4",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
|
||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.57.0",
|
||||
@@ -72,9 +72,9 @@
|
||||
"kysely-postgres-js": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.2",
|
||||
"nest-commander": "^3.11.1",
|
||||
"nestjs-cls": "^4.3.0",
|
||||
"nestjs-kysely": "^1.0.0",
|
||||
"nest-commander": "^3.16.0",
|
||||
"nestjs-cls": "^5.0.0",
|
||||
"nestjs-kysely": "^1.1.0",
|
||||
"nestjs-otel": "^6.0.0",
|
||||
"nodemailer": "^6.9.13",
|
||||
"openid-client": "^5.4.3",
|
||||
@@ -97,9 +97,9 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@nestjs/cli": "^10.1.16",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/testing": "^10.2.2",
|
||||
"@nestjs/cli": "^11.0.2",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.4",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@testcontainers/postgresql": "^10.2.1",
|
||||
"@types/archiver": "^6.0.0",
|
||||
|
||||
1
server/src/db.d.ts
vendored
1
server/src/db.d.ts
vendored
@@ -276,6 +276,7 @@ export interface Partners {
|
||||
|
||||
export interface Person {
|
||||
birthDate: Timestamp | null;
|
||||
color: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
faceAssetId: string | null;
|
||||
id: Generated<string>;
|
||||
|
||||
@@ -7,7 +7,14 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SourceType } from 'src/enum';
|
||||
import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
import {
|
||||
IsDateStringFormat,
|
||||
MaxDateString,
|
||||
Optional,
|
||||
ValidateBoolean,
|
||||
ValidateHexColor,
|
||||
ValidateUUID,
|
||||
} from 'src/validation';
|
||||
|
||||
export class PersonCreateDto {
|
||||
/**
|
||||
@@ -35,6 +42,10 @@ export class PersonCreateDto {
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
isFavorite?: boolean;
|
||||
|
||||
@Optional({ emptyToNull: true, nullable: true })
|
||||
@ValidateHexColor()
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
export class PersonUpdateDto extends PersonCreateDto {
|
||||
@@ -102,6 +113,8 @@ export class PersonResponseDto {
|
||||
updatedAt?: Date;
|
||||
@PropertyLifecycle({ addedAt: 'v1.126.0' })
|
||||
isFavorite?: boolean;
|
||||
@PropertyLifecycle({ addedAt: 'v1.126.0' })
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export class PersonWithFacesResponseDto extends PersonResponseDto {
|
||||
@@ -176,6 +189,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: person.isHidden,
|
||||
isFavorite: person.isFavorite,
|
||||
color: person.color ?? undefined,
|
||||
updatedAt: person.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { Optional, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TagCreateDto {
|
||||
@IsString()
|
||||
@@ -18,9 +17,8 @@ export class TagCreateDto {
|
||||
}
|
||||
|
||||
export class TagUpdateDto {
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
@IsHexColor()
|
||||
@Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value))
|
||||
@Optional({ emptyToNull: true, nullable: true })
|
||||
@ValidateHexColor()
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,14 @@ class PeopleUpdate {
|
||||
sidebarWeb?: boolean;
|
||||
}
|
||||
|
||||
class SharedLinksUpdate {
|
||||
@ValidateBoolean({ optional: true })
|
||||
enabled?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
sidebarWeb?: boolean;
|
||||
}
|
||||
|
||||
class TagsUpdate {
|
||||
@ValidateBoolean({ optional: true })
|
||||
enabled?: boolean;
|
||||
@@ -98,6 +106,11 @@ export class UserPreferencesUpdateDto {
|
||||
@Type(() => RatingsUpdate)
|
||||
ratings?: RatingsUpdate;
|
||||
|
||||
@Optional()
|
||||
@ValidateNested()
|
||||
@Type(() => SharedLinksUpdate)
|
||||
sharedLinks?: SharedLinksUpdate;
|
||||
|
||||
@Optional()
|
||||
@ValidateNested()
|
||||
@Type(() => TagsUpdate)
|
||||
@@ -152,6 +165,11 @@ class TagsResponse {
|
||||
sidebarWeb: boolean = true;
|
||||
}
|
||||
|
||||
class SharedLinksResponse {
|
||||
enabled: boolean = true;
|
||||
sidebarWeb: boolean = false;
|
||||
}
|
||||
|
||||
class EmailNotificationsResponse {
|
||||
enabled!: boolean;
|
||||
albumInvite!: boolean;
|
||||
@@ -175,6 +193,7 @@ export class UserPreferencesResponseDto implements UserPreferences {
|
||||
memories!: MemoriesResponse;
|
||||
people!: PeopleResponse;
|
||||
ratings!: RatingsResponse;
|
||||
sharedLinks!: SharedLinksResponse;
|
||||
tags!: TagsResponse;
|
||||
avatar!: AvatarResponse;
|
||||
emailNotifications!: EmailNotificationsResponse;
|
||||
|
||||
@@ -52,4 +52,7 @@ export class PersonEntity {
|
||||
|
||||
@Column({ default: false })
|
||||
isFavorite!: boolean;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, default: null })
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ export interface UserPreferences {
|
||||
ratings: {
|
||||
enabled: boolean;
|
||||
};
|
||||
sharedLinks: {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
};
|
||||
tags: {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
@@ -74,6 +78,10 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
|
||||
enabled: true,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
sharedLinks: {
|
||||
enabled: true,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
ratings: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
@@ -144,7 +144,6 @@ export interface IAssetDeleteJob extends IEntityJob {
|
||||
}
|
||||
|
||||
export interface ILibraryFileJob extends IEntityJob {
|
||||
ownerId: string;
|
||||
assetPath: string;
|
||||
}
|
||||
|
||||
|
||||
14
server/src/migrations/1738889177573-AddPersonColor.ts
Normal file
14
server/src/migrations/1738889177573-AddPersonColor.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddPersonColor1738889177573 implements MigrationInterface {
|
||||
name = 'AddPersonColor1738889177573'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD "color" character varying`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "color"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -216,6 +216,17 @@ export class JobRepository implements IJobRepository {
|
||||
|
||||
private getJobOptions(item: JobItem): JobsOptions | null {
|
||||
switch (item.name) {
|
||||
case JobName.LIBRARY_QUEUE_SYNC_ASSETS:
|
||||
case JobName.LIBRARY_QUEUE_SYNC_FILES:
|
||||
case JobName.LIBRARY_SYNC_ASSET:
|
||||
case JobName.LIBRARY_DELETE:
|
||||
case JobName.SIDECAR_SYNC:
|
||||
case JobName.SIDECAR_DISCOVERY: {
|
||||
return { jobId: `${item.data.id}-${item.name}` };
|
||||
}
|
||||
case JobName.LIBRARY_SYNC_FILE: {
|
||||
return { jobId: `${item.data.id}-${item.data.assetPath}` };
|
||||
}
|
||||
case JobName.NOTIFY_ALBUM_UPDATE: {
|
||||
return { jobId: item.data.id, delay: item.data?.delay };
|
||||
}
|
||||
@@ -228,6 +239,11 @@ export class JobRepository implements IJobRepository {
|
||||
case JobName.QUEUE_FACIAL_RECOGNITION: {
|
||||
return { jobId: JobName.QUEUE_FACIAL_RECOGNITION };
|
||||
}
|
||||
case JobName.LIBRARY_QUEUE_SYNC_ALL:
|
||||
case JobName.LIBRARY_QUEUE_CLEANUP: {
|
||||
// These jobs are globally unique and should only have one instance running at a time
|
||||
return { jobId: item.name };
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -181,7 +181,6 @@ describe(LibraryService.name, () => {
|
||||
name: JobName.LIBRARY_SYNC_FILE,
|
||||
data: {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: libraryStub.externalLibrary1.owner.id,
|
||||
assetPath: '/data/user1/photo.jpg',
|
||||
},
|
||||
},
|
||||
@@ -401,7 +400,6 @@ describe(LibraryService.name, () => {
|
||||
it('should import a new asset', async () => {
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: mockUser.id,
|
||||
assetPath: '/data/user1/photo.jpg',
|
||||
};
|
||||
|
||||
@@ -445,7 +443,6 @@ describe(LibraryService.name, () => {
|
||||
it('should import a new video', async () => {
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: mockUser.id,
|
||||
assetPath: '/data/user1/video.mp4',
|
||||
};
|
||||
|
||||
@@ -489,7 +486,6 @@ describe(LibraryService.name, () => {
|
||||
it('should not import an asset to a soft deleted library', async () => {
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: mockUser.id,
|
||||
assetPath: '/data/user1/photo.jpg',
|
||||
};
|
||||
|
||||
@@ -504,7 +500,6 @@ describe(LibraryService.name, () => {
|
||||
it('should not refresh a file whose mtime matches existing asset', async () => {
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: mockUser.id,
|
||||
assetPath: assetStub.hasFileExtension.originalPath,
|
||||
};
|
||||
|
||||
@@ -522,10 +517,9 @@ describe(LibraryService.name, () => {
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip existing asset', async () => {
|
||||
it('should skip an existing asset', async () => {
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: mockUser.id,
|
||||
assetPath: '/data/user1/photo.jpg',
|
||||
};
|
||||
|
||||
@@ -537,7 +531,6 @@ describe(LibraryService.name, () => {
|
||||
it('should not refresh an asset trashed by user', async () => {
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: mockUser.id,
|
||||
assetPath: assetStub.hasFileExtension.originalPath,
|
||||
};
|
||||
|
||||
@@ -554,7 +547,6 @@ describe(LibraryService.name, () => {
|
||||
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: userStub.admin.id,
|
||||
assetPath: '/data/user1/photo.jpg',
|
||||
};
|
||||
|
||||
@@ -572,7 +564,6 @@ describe(LibraryService.name, () => {
|
||||
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
ownerId: userStub.admin.id,
|
||||
assetPath: '/data/user1/photo.jpg',
|
||||
};
|
||||
|
||||
@@ -923,7 +914,6 @@ describe(LibraryService.name, () => {
|
||||
data: {
|
||||
id: libraryStub.externalLibraryWithImportPaths1.id,
|
||||
assetPath: '/foo/photo.jpg',
|
||||
ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -948,7 +938,6 @@ describe(LibraryService.name, () => {
|
||||
data: {
|
||||
id: libraryStub.externalLibraryWithImportPaths1.id,
|
||||
assetPath: '/foo/photo.jpg',
|
||||
ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -228,14 +228,13 @@ export class LibraryService extends BaseService {
|
||||
return mapLibrary(library);
|
||||
}
|
||||
|
||||
private async syncFiles({ id, ownerId }: LibraryEntity, assetPaths: string[]) {
|
||||
private async syncFiles({ id }: LibraryEntity, assetPaths: string[]) {
|
||||
await this.jobRepository.queueAll(
|
||||
assetPaths.map((assetPath) => ({
|
||||
name: JobName.LIBRARY_SYNC_FILE,
|
||||
data: {
|
||||
id,
|
||||
assetPath,
|
||||
ownerId,
|
||||
},
|
||||
})),
|
||||
);
|
||||
@@ -401,7 +400,7 @@ export class LibraryService extends BaseService {
|
||||
const mtime = stat.mtime;
|
||||
|
||||
asset = await this.assetRepository.create({
|
||||
ownerId: job.ownerId,
|
||||
ownerId: library.ownerId,
|
||||
libraryId: job.id,
|
||||
checksum: pathHash,
|
||||
originalPath: assetPath,
|
||||
@@ -433,12 +432,18 @@ export class LibraryService extends BaseService {
|
||||
async queueScan(id: string) {
|
||||
await this.findOrFail(id);
|
||||
|
||||
// We purge any existing scan jobs for this library. This is because the scan settings
|
||||
// might have changed and the user wants to start a new scan with these settings.
|
||||
await this.jobRepository.removeJob(id, JobName.LIBRARY_QUEUE_SYNC_FILES);
|
||||
await this.jobRepository.removeJob(id, JobName.LIBRARY_QUEUE_SYNC_ASSETS);
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.LIBRARY_QUEUE_SYNC_FILES,
|
||||
data: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
|
||||
}
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ describe(PersonService.name, () => {
|
||||
sut.reassignFaces(authStub.admin, personStub.noName.id, {
|
||||
data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }],
|
||||
}),
|
||||
).resolves.toEqual([personStub.noName]);
|
||||
).resolves.toBeDefined();
|
||||
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
@@ -448,7 +448,7 @@ describe(PersonService.name, () => {
|
||||
it('should create a new person', async () => {
|
||||
personMock.create.mockResolvedValue(personStub.primaryPerson);
|
||||
|
||||
await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson);
|
||||
await expect(sut.create(authStub.admin, {})).resolves.toBeDefined();
|
||||
|
||||
expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id });
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ export class PersonService extends BaseService {
|
||||
await this.personRepository.reassignFace(face.id, personId);
|
||||
}
|
||||
|
||||
result.push(person);
|
||||
result.push(mapPerson(person));
|
||||
}
|
||||
if (changeFeaturePhoto.length > 0) {
|
||||
// Remove duplicates
|
||||
@@ -178,20 +178,23 @@ export class PersonService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
|
||||
return this.personRepository.create({
|
||||
async create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
|
||||
const person = await this.personRepository.create({
|
||||
ownerId: auth.user.id,
|
||||
name: dto.name,
|
||||
birthDate: dto.birthDate,
|
||||
isHidden: dto.isHidden,
|
||||
isFavorite: dto.isFavorite,
|
||||
color: dto.color,
|
||||
});
|
||||
|
||||
return mapPerson(person);
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite } = dto;
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
if (assetId) {
|
||||
@@ -211,6 +214,7 @@ export class PersonService extends BaseService {
|
||||
birthDate,
|
||||
isHidden,
|
||||
isFavorite,
|
||||
color,
|
||||
});
|
||||
|
||||
if (assetId) {
|
||||
|
||||
@@ -31,6 +31,8 @@ describe(SearchService.name, () => {
|
||||
it('should pass options to search', async () => {
|
||||
const { name } = personStub.withName;
|
||||
|
||||
personMock.getByName.mockResolvedValue([]);
|
||||
|
||||
await sut.searchPerson(authStub.user1, { name, withHidden: false });
|
||||
|
||||
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import {
|
||||
mapPlaces,
|
||||
MetadataSearchDto,
|
||||
PlacesResponseDto,
|
||||
RandomSearchDto,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
SearchSuggestionRequestDto,
|
||||
SearchSuggestionType,
|
||||
SmartSearchDto,
|
||||
mapPlaces,
|
||||
} from 'src/dtos/search.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetOrder } from 'src/enum';
|
||||
@@ -24,7 +24,8 @@ import { isSmartSearchEnabled } from 'src/utils/misc';
|
||||
@Injectable()
|
||||
export class SearchService extends BaseService {
|
||||
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
||||
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
|
||||
const people = await this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
|
||||
return people.map((person) => mapPerson(person));
|
||||
}
|
||||
|
||||
async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsHexColor,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
@@ -97,6 +98,15 @@ export function Optional({ nullable, emptyToNull, ...validationOptions }: Option
|
||||
return applyDecorators(...decorators);
|
||||
}
|
||||
|
||||
export const ValidateHexColor = () => {
|
||||
const decorators = [
|
||||
IsHexColor(),
|
||||
Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)),
|
||||
];
|
||||
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean };
|
||||
export const ValidateUUID = (options?: UUIDOptions) => {
|
||||
const { optional, each, nullable } = { optional: false, each: false, nullable: false, ...options };
|
||||
|
||||
@@ -26,12 +26,14 @@
|
||||
controlable?: boolean;
|
||||
hideTextOnSmallScreen?: boolean;
|
||||
title?: string | undefined;
|
||||
position?: 'bottom-left' | 'bottom-right';
|
||||
onSelect: (option: T) => void;
|
||||
onClickOutside?: () => void;
|
||||
render?: (item: T) => string | RenderedOption;
|
||||
}
|
||||
|
||||
let {
|
||||
position = 'bottom-left',
|
||||
class: className = '',
|
||||
options,
|
||||
selectedOption = $bindable(options[0]),
|
||||
@@ -76,9 +78,24 @@
|
||||
};
|
||||
|
||||
let renderedSelectedOption = $derived(renderOption(selectedOption));
|
||||
|
||||
const getAlignClass = (position: 'bottom-left' | 'bottom-right') => {
|
||||
switch (position) {
|
||||
case 'bottom-left': {
|
||||
return 'left-0';
|
||||
}
|
||||
case 'bottom-right': {
|
||||
return 'right-0';
|
||||
}
|
||||
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}>
|
||||
<div use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }} class="relative">
|
||||
<!-- BUTTON TITLE -->
|
||||
<Button onclick={() => (showMenu = true)} fullWidth {title} variant="ghost" color="secondary" size="small">
|
||||
{#if renderedSelectedOption?.icon}
|
||||
@@ -91,7 +108,9 @@
|
||||
{#if showMenu}
|
||||
<div
|
||||
transition:fly={{ y: -30, duration: 250 }}
|
||||
class="text-sm font-medium fixed z-50 flex min-w-[250px] max-h-[70vh] overflow-y-auto immich-scrollbar flex-col rounded-2xl bg-gray-100 py-2 text-black shadow-lg dark:bg-gray-700 dark:text-white {className}"
|
||||
class="text-sm font-medium absolute z-50 flex min-w-[250px] max-h-[70vh] overflow-y-auto immich-scrollbar flex-col rounded-2xl bg-gray-100 py-2 text-black shadow-lg dark:bg-gray-700 dark:text-white {className} {getAlignClass(
|
||||
position,
|
||||
)}"
|
||||
>
|
||||
{#each options as option (option)}
|
||||
{@const renderedOption = renderOption(option)}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
interface Props {
|
||||
filters: string[];
|
||||
labels?: string[];
|
||||
selected: string;
|
||||
label: string;
|
||||
onSelect: (selected: string) => void;
|
||||
}
|
||||
|
||||
let { filters, selected, label, onSelect }: Props = $props();
|
||||
let { filters, selected, label, labels, onSelect }: Props = $props();
|
||||
|
||||
const id = `group-tab-${generateId()}`;
|
||||
</script>
|
||||
@@ -32,7 +33,7 @@
|
||||
for="{id}-{index}"
|
||||
class="flex h-full cursor-pointer items-center px-4 text-sm hover:bg-gray-300 group-first-of-type:rounded-s-2xl group-last-of-type:rounded-e-2xl peer-checked:bg-gray-300 dark:hover:bg-gray-800 peer-checked:dark:bg-gray-700"
|
||||
>
|
||||
{filter}
|
||||
{labels?.[index] ?? filter}
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
67
web/src/lib/components/places-page/places-card-group.svelte
Normal file
67
web/src/lib/components/places-page/places-card-group.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk';
|
||||
import { placesViewSettings } from '$lib/stores/preferences.store';
|
||||
import { type PlacesGroup, isPlacesGroupCollapsed, togglePlacesGroupCollapsing } from '$lib/utils/places-utils';
|
||||
import { mdiChevronRight } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
|
||||
interface Props {
|
||||
places: AssetResponseDto[];
|
||||
group?: PlacesGroup | undefined;
|
||||
}
|
||||
|
||||
let { places, group = undefined }: Props = $props();
|
||||
|
||||
let isCollapsed = $derived(!!group && isPlacesGroupCollapsed($placesViewSettings, group.id));
|
||||
let iconRotation = $derived(isCollapsed ? 'rotate-0' : 'rotate-90');
|
||||
</script>
|
||||
|
||||
{#if group}
|
||||
<div class="grid">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => togglePlacesGroupCollapsing(group.id)}
|
||||
class="w-fit mt-2 pt-2 pr-2 mb-2 dark:text-immich-dark-fg"
|
||||
aria-expanded={!isCollapsed}
|
||||
>
|
||||
<Icon
|
||||
path={mdiChevronRight}
|
||||
size="24"
|
||||
class="inline-block -mt-2.5 transition-all duration-[250ms] {iconRotation}"
|
||||
/>
|
||||
<span class="font-bold text-3xl text-black dark:text-white">{group.name}</span>
|
||||
<span class="ml-1.5">({$t('places_count', { values: { count: places.length } })})</span>
|
||||
</button>
|
||||
<hr class="dark:border-immich-dark-gray" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4">
|
||||
{#if !isCollapsed}
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each places as item}
|
||||
{@const city = item.exifInfo?.city}
|
||||
<a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city })}" draggable="false">
|
||||
<div
|
||||
class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center overflow-hidden rounded-xl brightness-75 filter"
|
||||
>
|
||||
<img
|
||||
src={getAssetThumbnailUrl({ id: item.id, size: AssetMediaSize.Thumbnail })}
|
||||
alt={city}
|
||||
class="object-cover w-[156px] h-[156px]"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
|
||||
>
|
||||
{city}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
93
web/src/lib/components/places-page/places-controls.svelte
Normal file
93
web/src/lib/components/places-page/places-controls.svelte
Normal file
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { IconButton } from '@immich/ui';
|
||||
import Dropdown from '$lib/components/elements/dropdown.svelte';
|
||||
import SearchBar from '$lib/components/elements/search-bar.svelte';
|
||||
import { PlacesGroupBy, placesViewSettings } from '$lib/stores/preferences.store';
|
||||
import {
|
||||
mdiFolderArrowUpOutline,
|
||||
mdiFolderRemoveOutline,
|
||||
mdiUnfoldLessHorizontal,
|
||||
mdiUnfoldMoreHorizontal,
|
||||
} from '@mdi/js';
|
||||
import {
|
||||
type PlacesGroupOptionMetadata,
|
||||
findGroupOptionMetadata,
|
||||
getSelectedPlacesGroupOption,
|
||||
groupOptionsMetadata,
|
||||
expandAllPlacesGroups,
|
||||
collapseAllPlacesGroups,
|
||||
} from '$lib/utils/places-utils';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
placesGroups: string[];
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
let { placesGroups, searchQuery = $bindable() }: Props = $props();
|
||||
|
||||
const handleChangeGroupBy = ({ id }: PlacesGroupOptionMetadata) => {
|
||||
$placesViewSettings.groupBy = id;
|
||||
};
|
||||
|
||||
let groupIcon = $derived.by(() => {
|
||||
return selectedGroupOption.id === PlacesGroupBy.None ? mdiFolderRemoveOutline : mdiFolderArrowUpOutline; // OR mdiFolderArrowDownOutline
|
||||
});
|
||||
|
||||
let selectedGroupOption = $derived(findGroupOptionMetadata($placesViewSettings.groupBy));
|
||||
|
||||
let placesGroupByNames: Record<PlacesGroupBy, string> = $derived({
|
||||
[PlacesGroupBy.None]: $t('group_no'),
|
||||
[PlacesGroupBy.Country]: $t('group_country'),
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Search Places -->
|
||||
<div class="hidden md:block h-10 xl:w-60 2xl:w-80">
|
||||
<SearchBar placeholder={$t('search_places')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
</div>
|
||||
|
||||
<!-- Group Places -->
|
||||
<Dropdown
|
||||
position="bottom-right"
|
||||
title={$t('group_places_by')}
|
||||
options={Object.values(groupOptionsMetadata)}
|
||||
selectedOption={selectedGroupOption}
|
||||
onSelect={handleChangeGroupBy}
|
||||
render={({ id, isDisabled }) => ({
|
||||
title: placesGroupByNames[id],
|
||||
icon: groupIcon,
|
||||
disabled: isDisabled(),
|
||||
})}
|
||||
/>
|
||||
|
||||
{#if getSelectedPlacesGroupOption($placesViewSettings) !== PlacesGroupBy.None}
|
||||
<span in:fly={{ x: -50, duration: 250 }}>
|
||||
<!-- Expand Countries Groups -->
|
||||
<div class="hidden xl:flex gap-0">
|
||||
<div class="block">
|
||||
<IconButton
|
||||
title={$t('expand_all')}
|
||||
onclick={() => expandAllPlacesGroups()}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
icon={mdiUnfoldMoreHorizontal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Collapse Countries Groups -->
|
||||
<div class="block">
|
||||
<IconButton
|
||||
title={$t('collapse_all')}
|
||||
onclick={() => collapseAllPlacesGroups(placesGroups)}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
icon={mdiUnfoldLessHorizontal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
121
web/src/lib/components/places-page/places-list.svelte
Normal file
121
web/src/lib/components/places-page/places-list.svelte
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
import PlacesCardGroup from './places-card-group.svelte';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { mdiMapMarkerOff } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { PlacesGroupBy, type PlacesViewSettings } from '$lib/stores/preferences.store';
|
||||
|
||||
import { type PlacesGroup, getSelectedPlacesGroupOption } from '$lib/utils/places-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
interface Props {
|
||||
places?: AssetResponseDto[];
|
||||
searchQuery?: string;
|
||||
searchResultCount: number;
|
||||
userSettings: PlacesViewSettings;
|
||||
placesGroupIds?: string[];
|
||||
}
|
||||
|
||||
let {
|
||||
places = $bindable([]),
|
||||
searchQuery = '',
|
||||
searchResultCount = $bindable(0),
|
||||
userSettings,
|
||||
placesGroupIds = $bindable([]),
|
||||
}: Props = $props();
|
||||
|
||||
interface PlacesGroupOption {
|
||||
[option: string]: (places: AssetResponseDto[]) => PlacesGroup[];
|
||||
}
|
||||
|
||||
const groupOptions: PlacesGroupOption = {
|
||||
/** No grouping */
|
||||
[PlacesGroupBy.None]: (places): PlacesGroup[] => {
|
||||
return [
|
||||
{
|
||||
id: $t('places'),
|
||||
name: $t('places'),
|
||||
places,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
/** Group by year */
|
||||
[PlacesGroupBy.Country]: (places): PlacesGroup[] => {
|
||||
const unknownCountry = $t('unknown_country');
|
||||
|
||||
const groupedByCountry = groupBy(places, (place) => {
|
||||
return place.exifInfo?.country ?? unknownCountry;
|
||||
});
|
||||
|
||||
const sortedByCountryName = Object.entries(groupedByCountry).sort(([a], [b]) => {
|
||||
// We make sure empty albums stay at the end of the list
|
||||
if (a === unknownCountry) {
|
||||
return 1;
|
||||
} else if (b === unknownCountry) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
});
|
||||
|
||||
return sortedByCountryName.map(([country, places]) => ({
|
||||
id: country,
|
||||
name: country,
|
||||
places,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
let filteredPlaces: AssetResponseDto[] = $state([]);
|
||||
let groupedPlaces: PlacesGroup[] = $state([]);
|
||||
|
||||
let placesGroupOption: string = $state(PlacesGroupBy.None);
|
||||
|
||||
let hasPlaces = $derived(places.length > 0);
|
||||
|
||||
// Step 1: Filter using the given search query.
|
||||
run(() => {
|
||||
if (searchQuery) {
|
||||
const searchQueryNormalized = normalizeSearchString(searchQuery);
|
||||
|
||||
filteredPlaces = places.filter((place) => {
|
||||
return normalizeSearchString(place.exifInfo?.city ?? '').includes(searchQueryNormalized);
|
||||
});
|
||||
} else {
|
||||
filteredPlaces = places;
|
||||
}
|
||||
|
||||
searchResultCount = filteredPlaces.length;
|
||||
});
|
||||
|
||||
// Step 2: Group places.
|
||||
run(() => {
|
||||
placesGroupOption = getSelectedPlacesGroupOption(userSettings);
|
||||
const groupFunc = groupOptions[placesGroupOption] ?? groupOptions[PlacesGroupBy.None];
|
||||
groupedPlaces = groupFunc(filteredPlaces);
|
||||
|
||||
placesGroupIds = groupedPlaces.map(({ id }) => id);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if hasPlaces}
|
||||
<!-- Album Cards -->
|
||||
{#if placesGroupOption === PlacesGroupBy.None}
|
||||
<PlacesCardGroup places={groupedPlaces[0].places} />
|
||||
{:else}
|
||||
{#each groupedPlaces as placeGroup (placeGroup.id)}
|
||||
<PlacesCardGroup places={placeGroup.places} group={placeGroup} />
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<Icon path={mdiMapMarkerOff} size="3.5em" />
|
||||
<p class="mt-5 text-3xl font-medium">{$t('no_places')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -21,6 +21,7 @@
|
||||
mdiToolboxOutline,
|
||||
mdiFolderOutline,
|
||||
mdiTagMultipleOutline,
|
||||
mdiLink,
|
||||
} from '@mdi/js';
|
||||
import SideBarSection from './side-bar-section.svelte';
|
||||
import SideBarLink from './side-bar-link.svelte';
|
||||
@@ -72,6 +73,10 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
|
||||
<SideBarLink title={$t('shared_links')} routeId="/(user)/shared-links" icon={mdiLink} />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('sharing')}
|
||||
routeId="/(user)/sharing"
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { mdiCircleEditOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
menuItem?: boolean;
|
||||
onEdit: () => void;
|
||||
sharedLink: SharedLinkResponseDto;
|
||||
}
|
||||
|
||||
let { menuItem = false, onEdit }: Props = $props();
|
||||
let { sharedLink, menuItem = false }: Props = $props();
|
||||
|
||||
const onEdit = async () => {
|
||||
await goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if menuItem}
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
interface Props {
|
||||
link: SharedLinkResponseDto;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
let { link, onDelete, onEdit }: Props = $props();
|
||||
let { link, onDelete }: Props = $props();
|
||||
|
||||
let now = DateTime.now();
|
||||
let expiresAt = $derived(link.expiresAt ? DateTime.fromISO(link.expiresAt) : undefined);
|
||||
@@ -95,10 +94,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</svelte:element>
|
||||
|
||||
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
|
||||
<div class="sm:flex hidden">
|
||||
<SharedLinkEdit {onEdit} />
|
||||
<SharedLinkEdit sharedLink={link} />
|
||||
<SharedLinkCopy {link} />
|
||||
<SharedLinkDelete {onDelete} />
|
||||
</div>
|
||||
@@ -112,7 +110,7 @@
|
||||
padding="3"
|
||||
hideContent
|
||||
>
|
||||
<SharedLinkEdit menuItem {onEdit} />
|
||||
<SharedLinkEdit menuItem sharedLink={link} />
|
||||
<SharedLinkCopy menuItem {link} />
|
||||
<SharedLinkDelete menuItem {onDelete} />
|
||||
</ButtonContextMenu>
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
// Ratings
|
||||
let ratingsEnabled = $state($preferences?.ratings?.enabled ?? false);
|
||||
|
||||
// Shared links
|
||||
let sharedLinksEnabled = $state($preferences?.sharedLinks?.enabled ?? true);
|
||||
let sharedLinkSidebar = $state($preferences?.sharedLinks?.sidebarWeb ?? false);
|
||||
|
||||
// Tags
|
||||
let tagsEnabled = $state($preferences?.tags?.enabled ?? false);
|
||||
let tagsSidebar = $state($preferences?.tags?.sidebarWeb ?? false);
|
||||
@@ -39,6 +43,7 @@
|
||||
memories: { enabled: memoriesEnabled },
|
||||
people: { enabled: peopleEnabled, sidebarWeb: peopleSidebar },
|
||||
ratings: { enabled: ratingsEnabled },
|
||||
sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar },
|
||||
tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar },
|
||||
},
|
||||
});
|
||||
@@ -104,6 +109,21 @@
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion key="shared-links" title={$t('shared_links')} subtitle={$t('shared_links_description')}>
|
||||
<div class="ml-4 mt-6">
|
||||
<SettingSwitch title={$t('enable')} bind:checked={sharedLinksEnabled} />
|
||||
</div>
|
||||
{#if sharedLinksEnabled}
|
||||
<div class="ml-4 mt-6">
|
||||
<SettingSwitch
|
||||
title={$t('sidebar')}
|
||||
subtitle={$t('sidebar_display_description')}
|
||||
bind:checked={sharedLinkSidebar}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion key="tags" title={$t('tags')} subtitle={$t('tag_feature_description')}>
|
||||
<div class="ml-4 mt-6">
|
||||
<SettingSwitch title={$t('enable')} bind:checked={tagsEnabled} />
|
||||
|
||||
@@ -30,7 +30,7 @@ export enum AppRoute {
|
||||
EXPLORE = '/explore',
|
||||
SHARE = '/share',
|
||||
SHARING = '/sharing',
|
||||
SHARED_LINKS = '/sharing/sharedlinks',
|
||||
SHARED_LINKS = '/shared-links',
|
||||
SEARCH = '/search',
|
||||
MAP = '/map',
|
||||
USER_SETTINGS = '/user-settings',
|
||||
|
||||
@@ -101,6 +101,14 @@ export interface AlbumViewSettings {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlacesViewSettings {
|
||||
groupBy: string;
|
||||
collapsedGroups: {
|
||||
// Grouping Option => Array<Group ID>
|
||||
[group: string]: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SidebarSettings {
|
||||
people: boolean;
|
||||
sharing: boolean;
|
||||
@@ -147,6 +155,16 @@ export const albumViewSettings = persisted<AlbumViewSettings>('album-view-settin
|
||||
collapsedGroups: {},
|
||||
});
|
||||
|
||||
export enum PlacesGroupBy {
|
||||
None = 'None',
|
||||
Country = 'Country',
|
||||
}
|
||||
|
||||
export const placesViewSettings = persisted<PlacesViewSettings>('places-view-settings', {
|
||||
groupBy: PlacesGroupBy.None,
|
||||
collapsedGroups: {},
|
||||
});
|
||||
|
||||
export const showDeleteModal = persisted<boolean>('delete-confirm-dialog', true, {});
|
||||
|
||||
export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-file', false, {});
|
||||
|
||||
95
web/src/lib/utils/places-utils.ts
Normal file
95
web/src/lib/utils/places-utils.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { PlacesGroupBy, placesViewSettings, type PlacesViewSettings } from '$lib/stores/preferences.store';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
/**
|
||||
* --------------
|
||||
* Places Grouping
|
||||
* --------------
|
||||
*/
|
||||
export interface PlacesGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
places: AssetResponseDto[];
|
||||
}
|
||||
|
||||
export interface PlacesGroupOptionMetadata {
|
||||
id: PlacesGroupBy;
|
||||
isDisabled: () => boolean;
|
||||
}
|
||||
|
||||
export const groupOptionsMetadata: PlacesGroupOptionMetadata[] = [
|
||||
{
|
||||
id: PlacesGroupBy.None,
|
||||
isDisabled: () => false,
|
||||
},
|
||||
{
|
||||
id: PlacesGroupBy.Country,
|
||||
isDisabled: () => false,
|
||||
},
|
||||
];
|
||||
|
||||
export const findGroupOptionMetadata = (groupBy: string) => {
|
||||
// Default is no grouping
|
||||
const defaultGroupOption = groupOptionsMetadata[0];
|
||||
return groupOptionsMetadata.find(({ id }) => groupBy === id) ?? defaultGroupOption;
|
||||
};
|
||||
|
||||
export const getSelectedPlacesGroupOption = (settings: PlacesViewSettings) => {
|
||||
const defaultGroupOption = PlacesGroupBy.None;
|
||||
const albumGroupOption = settings.groupBy ?? defaultGroupOption;
|
||||
|
||||
if (findGroupOptionMetadata(albumGroupOption).isDisabled()) {
|
||||
return defaultGroupOption;
|
||||
}
|
||||
return albumGroupOption;
|
||||
};
|
||||
|
||||
/**
|
||||
* ----------------------------
|
||||
* Places Groups Collapse/Expand
|
||||
* ----------------------------
|
||||
*/
|
||||
const getCollapsedPlacesGroups = (settings: PlacesViewSettings) => {
|
||||
settings.collapsedGroups ??= {};
|
||||
const { collapsedGroups, groupBy } = settings;
|
||||
collapsedGroups[groupBy] ??= [];
|
||||
return collapsedGroups[groupBy];
|
||||
};
|
||||
|
||||
export const isPlacesGroupCollapsed = (settings: PlacesViewSettings, groupId: string) => {
|
||||
if (settings.groupBy === PlacesGroupBy.None) {
|
||||
return false;
|
||||
}
|
||||
return getCollapsedPlacesGroups(settings).includes(groupId);
|
||||
};
|
||||
|
||||
export const togglePlacesGroupCollapsing = (groupId: string) => {
|
||||
const settings = get(placesViewSettings);
|
||||
if (settings.groupBy === PlacesGroupBy.None) {
|
||||
return;
|
||||
}
|
||||
const collapsedGroups = getCollapsedPlacesGroups(settings);
|
||||
const groupIndex = collapsedGroups.indexOf(groupId);
|
||||
if (groupIndex === -1) {
|
||||
// Collapse
|
||||
collapsedGroups.push(groupId);
|
||||
} else {
|
||||
// Expand
|
||||
collapsedGroups.splice(groupIndex, 1);
|
||||
}
|
||||
placesViewSettings.set(settings);
|
||||
};
|
||||
|
||||
export const collapseAllPlacesGroups = (groupIds: string[]) => {
|
||||
placesViewSettings.update((settings) => {
|
||||
const collapsedGroups = getCollapsedPlacesGroups(settings);
|
||||
collapsedGroups.length = 0;
|
||||
collapsedGroups.push(...groupIds);
|
||||
return settings;
|
||||
});
|
||||
};
|
||||
|
||||
export const expandAllPlacesGroups = () => {
|
||||
collapseAllPlacesGroups([]);
|
||||
};
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { mdiMapMarkerOff } from '@mdi/js';
|
||||
import PlacesControls from '$lib/components/places-page/places-controls.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import Places from '$lib/components/places-page/places-list.svelte';
|
||||
import { placesViewSettings } from '$lib/stores/preferences.store';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@@ -21,43 +20,33 @@
|
||||
};
|
||||
};
|
||||
|
||||
let searchQuery = $state('');
|
||||
let searchResultCount = $state(0);
|
||||
let placesGroups: string[] = $state([]);
|
||||
|
||||
let places = $derived(data.items.filter((item): item is AssetWithCity => !!item.exifInfo?.city));
|
||||
let hasPlaces = $derived(places.length > 0);
|
||||
let countVisiblePlaces = $derived(searchQuery ? searchResultCount : places.length);
|
||||
|
||||
let innerHeight: number = $state(0);
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight />
|
||||
|
||||
<UserPageLayout title={$t('places')}>
|
||||
{#if hasPlaces}
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each places as item (item.id)}
|
||||
{@const city = item.exifInfo.city}
|
||||
<a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city })}" draggable="false">
|
||||
<div
|
||||
class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center overflow-hidden rounded-xl brightness-75 filter"
|
||||
>
|
||||
<img
|
||||
src={getAssetThumbnailUrl({ id: item.id, size: AssetMediaSize.Thumbnail })}
|
||||
alt={city}
|
||||
class="object-cover w-[156px] h-[156px]"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
|
||||
>
|
||||
{city}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
<UserPageLayout
|
||||
title={$t('places')}
|
||||
description={countVisiblePlaces === 0 && !searchQuery ? undefined : `(${countVisiblePlaces.toLocaleString($locale)})`}
|
||||
>
|
||||
{#snippet buttons()}
|
||||
<div class="flex place-items-center gap-2">
|
||||
<PlacesControls {placesGroups} bind:searchQuery />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<Icon path={mdiMapMarkerOff} size="3.5em" />
|
||||
<p class="mt-5 text-3xl font-medium">{$t('no_places')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<Places
|
||||
{places}
|
||||
userSettings={$placesViewSettings}
|
||||
{searchQuery}
|
||||
bind:searchResultCount
|
||||
bind:placesGroupIds={placesGroups}
|
||||
/>
|
||||
</UserPageLayout>
|
||||
|
||||
119
web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte
Normal file
119
web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte
Normal file
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import GroupTab from '$lib/components/elements/group-tab.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllSharedLinks, removeSharedLink, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
type Props = {
|
||||
data: PageData;
|
||||
};
|
||||
|
||||
const { data }: Props = $props();
|
||||
|
||||
let sharedLinks: SharedLinkResponseDto[] = $state([]);
|
||||
let sharedLink = $derived(sharedLinks.find(({ id }) => id === page.params.id));
|
||||
|
||||
const refresh = async () => {
|
||||
sharedLinks = await getAllSharedLinks();
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await refresh();
|
||||
});
|
||||
|
||||
const handleDeleteLink = async (id: string) => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
title: $t('delete_shared_link'),
|
||||
prompt: $t('confirm_delete_shared_link'),
|
||||
confirmText: $t('delete'),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeSharedLink({ id });
|
||||
notificationController.show({ message: $t('deleted_shared_link'), type: NotificationType.Info });
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_shared_link'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditDone = async () => {
|
||||
await refresh();
|
||||
await goto(AppRoute.SHARED_LINKS);
|
||||
};
|
||||
|
||||
type Filter = 'all' | 'album' | 'individual';
|
||||
|
||||
const filterMap: Record<Filter, string> = {
|
||||
all: $t('all'),
|
||||
album: $t('albums'),
|
||||
individual: $t('individual_shares'),
|
||||
};
|
||||
|
||||
let filters = Object.keys(filterMap);
|
||||
let labels = Object.values(filterMap);
|
||||
|
||||
const getActiveTab = (url: URL) => {
|
||||
const filter = url.searchParams.get('filter');
|
||||
return filter && filters.includes(filter) ? filter : 'all';
|
||||
};
|
||||
|
||||
let selectedTab = $derived(getActiveTab(page.url));
|
||||
const handleSelectTab = async (value: string) => {
|
||||
await goto(`${AppRoute.SHARED_LINKS}?filter=${value}`);
|
||||
};
|
||||
|
||||
let filteredSharedLinks = $derived(
|
||||
sharedLinks.filter(
|
||||
({ type }) =>
|
||||
selectedTab === 'all' ||
|
||||
(type === SharedLinkType.Album && selectedTab === 'album') ||
|
||||
(type === SharedLinkType.Individual && selectedTab === 'individual'),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
{#snippet buttons()}
|
||||
<div class="hidden xl:block h-10">
|
||||
<GroupTab label={$t('show_shared_links')} {filters} {labels} selected={selectedTab} onSelect={handleSelectTab} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div>
|
||||
{#if sharedLinks.length === 0}
|
||||
<div
|
||||
class="flex place-content-center place-items-center rounded-lg bg-gray-100 dark:bg-immich-dark-gray dark:text-immich-gray p-12"
|
||||
>
|
||||
<p>{$t('you_dont_have_any_shared_links')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each filteredSharedLinks as link (link.id)}
|
||||
<SharedLinkCard {link} onDelete={() => handleDeleteLink(link.id)} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink}
|
||||
<CreateSharedLinkModal editingLink={sharedLink} onClose={handleEditDone} />
|
||||
{/if}
|
||||
</div>
|
||||
</UserPageLayout>
|
||||
14
web/src/routes/(user)/shared-links/[[id=id]]/+page.ts
Normal file
14
web/src/routes/(user)/shared-links/[[id=id]]/+page.ts
Normal 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('shared_links'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,89 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto, afterNavigate } from '$app/navigation';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllSharedLinks, removeSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { mdiArrowLeft } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let sharedLinks: SharedLinkResponseDto[] = $state([]);
|
||||
let editSharedLink: SharedLinkResponseDto | null = $state(null);
|
||||
|
||||
const refresh = async () => {
|
||||
sharedLinks = await getAllSharedLinks();
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await refresh();
|
||||
});
|
||||
|
||||
const handleDeleteLink = async (id: string) => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
title: $t('delete_shared_link'),
|
||||
prompt: $t('confirm_delete_shared_link'),
|
||||
confirmText: $t('delete'),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeSharedLink({ id });
|
||||
notificationController.show({ message: $t('deleted_shared_link'), type: NotificationType.Info });
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_shared_link'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditDone = async () => {
|
||||
await refresh();
|
||||
editSharedLink = null;
|
||||
};
|
||||
|
||||
let backUrl: string = AppRoute.SHARING;
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
let url: string | undefined = from?.url?.pathname;
|
||||
backUrl = url || AppRoute.SHARING;
|
||||
});
|
||||
</script>
|
||||
|
||||
<ControlAppBar backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
|
||||
{#snippet leading()}
|
||||
{$t('shared_links')}
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
|
||||
<section class="mt-[120px] flex flex-col pb-[120px] container max-w-screen-lg mx-auto px-3">
|
||||
<div class="mb-4 dark:text-immich-gray">
|
||||
<p>{$t('manage_shared_links')}</p>
|
||||
</div>
|
||||
{#if sharedLinks.length === 0}
|
||||
<div
|
||||
class="flex place-content-center place-items-center rounded-lg bg-gray-100 dark:bg-immich-dark-gray dark:text-immich-gray p-12"
|
||||
>
|
||||
<p>{$t('you_dont_have_any_shared_links')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
{#each sharedLinks as link (link.id)}
|
||||
<SharedLinkCard {link} onDelete={() => handleDeleteLink(link.id)} onEdit={() => (editSharedLink = link)} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if editSharedLink}
|
||||
<CreateSharedLinkModal editingLink={editSharedLink} onClose={handleEditDone} />
|
||||
{/if}
|
||||
@@ -1,14 +1,7 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
await authenticate();
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: $t('shared_links'),
|
||||
},
|
||||
};
|
||||
export const load = (() => {
|
||||
redirect(307, AppRoute.SHARED_LINKS);
|
||||
}) satisfies PageLoad;
|
||||
|
||||
Reference in New Issue
Block a user