1
0
forked from Cutlery/immich

Compare commits

..

15 Commits

Author SHA1 Message Date
martabal 8fd54e3994 Merge branch 'main' into fix/edit-faces-notification 2024-04-08 19:58:32 +02:00
martabal ad9aafd484 Merge branch 'main' into fix/edit-faces-notification 2024-04-06 18:52:53 +02:00
martabal 8eca25007e Merge branch 'main' into fix/edit-faces-notification 2024-04-02 19:30:27 +02:00
martabal 86b200e870 Merge branch 'main' into fix/edit-faces-notification 2024-03-19 18:37:43 +01:00
martabal 5dcc4ddfc6 Merge branch 'main' into fix/edit-faces-notification 2024-03-18 20:35:59 +01:00
martabal 6b10994bd2 Merge branch 'main' into fix/edit-faces-notification 2024-03-13 23:32:40 +01:00
martabal 12ede06411 Merge branch 'main' into fix/edit-faces-notification 2024-03-11 23:02:02 +01:00
martabal 121e9f1f1c merge main 2024-03-08 22:50:34 +01:00
martabal c263607515 Merge branch 'main' into fix/edit-faces-notification 2024-03-03 15:04:50 +01:00
martabal 4cb2e16549 merge main 2024-02-27 21:03:12 +01:00
martabal 0f8fb8d38b Merge branch 'main' into fix/edit-faces-notification 2024-02-27 08:02:51 +01:00
martabal 107157b856 rename 2024-02-27 08:02:47 +01:00
martabal 87cbcc02c3 fix: use id instead of index 2024-02-23 00:46:41 +01:00
martabal 62531a75eb fix: lint 2024-02-22 18:48:51 +01:00
martabal 29c7663caa fix: notification number of people when editing faces 2024-02-22 18:41:17 +01:00
87 changed files with 1139 additions and 1055 deletions
@@ -6,14 +6,13 @@ body:
attributes:
value: |
Please use this form to request new feature for Immich
- type: checkboxes
attributes:
label: I have searched the existing feature requests to make sure this is not a duplicate request.
options:
- label: "Yes"
- label: I have searched the existing feature requests to make sure this is not a duplicate request.
required: true
- type: textarea
id: feature
attributes:
-4
View File
@@ -1,4 +0,0 @@
/.github/ @bo0tzz
/docker/ @bo0tzz
/server/ @danieldietzler
/e2e/ @danieldietzler
+2 -1
View File
@@ -69,8 +69,9 @@ services:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
- pgdata:/var/lib/postgresql/data
restart: always
volumes:
pgdata:
model-cache:
-1
View File
@@ -14,6 +14,5 @@ DB_PASSWORD=postgres
DB_HOSTNAME=immich_postgres
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
DB_DATA_LOCATION=./postgres
REDIS_HOSTNAME=immich_redis
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+12 -52
View File
@@ -1,57 +1,17 @@
# Partner Sharing
Immich allows you to share your library with other users. They can then view your library and download the assets.
You can manage one or multiple users to have access to your library from the [User Settings](docs/features/user-settings.md) page.
<img src={require('./img/partner-sharing-1.png').default} title='Partner Sharing 1' />
<img src={require('./img/partner-sharing-2.png').default} title='Partner Sharing 2' />
Accessing the shared library can be done from the Sharing page.
<img src={require('./img/partner-sharing-3.png').default} title='Partner Sharing 3' />
:::tip Sharing specific assets
For sharing a specific set of assets, you can use the shared album feature of Immich.
:::
Immich allows you to share your library with other users. They can then view your library and download the assets. You can manage Partner Sharing from the [User Settings](docs/features/user-settings.md) page on the web.
Partner Sharing includes:
- Access to all non-archived and trashed photos and videos.
- Access to all metadata, including GPS information.
- Access to share assets via shared links, albums, etc.
:::info
Partner sharing is one-way. To view your partner's assets, they must also share them with you.
:::
## Sharing with a Partner
:::note Duplicates
Partner sharing may result in displaying duplicate assets on the main timeline.
:::
<img src={require('./img/partner-sharing-1.png').default} width="70%" title='Add Partner 1' />
<img src={require('./img/partner-sharing-2.png').default} width="70%" title='Add Partner 2' />
<img src={require('./img/partner-sharing-4.png').default} width="70%" title='Add Partner 4' />
## Viewing Partner Assets
Access partner assets via the Sharing page.
<img src={require('./img/partner-sharing-3.png').default} width="70%" title='Access to the Shared Library' />
## Timeline Integration
Partner shared photos can be displayed in the main timeline. This feature can be enabled on a per-partner basis and can be viewed and updated on both the web and mobile app.
### Web:
Account Settings -> Sharing -> Show in timeline
<img src={require('./img/partner-sharing-5.png').default} width="70%" title='Partner Sharing for the web interface' />
### Mobile App:
From the partners view, on the top right corner of the app bar
<img src={require('./img/partner-sharing-6.png').default} width="30%" title='Partner Sharing for the mobile app' />
## Removing Access
In order to remove a partner, you can go to User -> Account Settings -> Sharing and click on the X button.
<img src={require('./img/partner-sharing-7.png').default} width="70%" title='Remove Partner' />
+1 -1
View File
@@ -8,7 +8,7 @@ During Exif Extraction, assets with latitudes and longitudes are reverse geocode
## Usage
Data from a reverse geocode is displayed in the image details, and used in [Smart Search](/docs/features/smart-search.md).
Data from a reverse geocode is displayed in the image details, and used in [Search](/docs/features/search.md).
<img src={require('./img/reverse-geocoding-mobile1.png').default} width='33%' title='Reverse Geocoding' />
<img src={require('./img/reverse-geocoding-mobile2.png').default} width='33%' title='Reverse Geocoding' />
+14
View File
@@ -0,0 +1,14 @@
# Search
Immich uses Postgres as its search database for both metadata and smart search.
Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like CLIP to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
Metadata search (prefixed with `m:`) can search specifically by text without the use of a model.
Archived photos are not included in search results by default. To include them, add the query parameter `withArchived=true` to the url.
Some search examples:
<img src={require('./img/search-ex-2.webp').default} title='Search Example 1' />
<img src={require('./img/search-ex-3.webp').default} title='Search Example 2' />
-49
View File
@@ -1,49 +0,0 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Smart Search
Immich uses Postgres as its search database for both metadata and smart search.
Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
Archived photos are not included in search results by default. To include them, mark the checkbox in [advanced search filters](/docs/features/smart-search#advanced-search-filters).
:::tip Alternative CLIP Models
More powerful models can be used for more accurate search results. For more information, see the related [FAQ](/docs/FAQ#can-i-use-a-custom-clip-model).
:::
:::info
Smart Search is currently limited to 5,000 results for a single search on the web.
:::
## Advanced Search Filters
In addition, Immich offers advanced search functionality, allowing you to find specific content using customizable search filters. These filters include location, one or more faces, specific albums, and more. You can try out the search filters on the [Demo site](https://demo.immich.app).
Smart search features include:
- Search for one or more faces (with or without context search).
- Search by Country or State or City or by all three.
- Search by camera make and model.
- Search by date range.
- Search by file name.
- Search by media types: image, video or all (**Note:** Image includes live images).
- Search by condition: not in any album or archive or Favorite or all conditions.
<Tabs>
<TabItem value="Computer" label="Computer" default>
Some search examples:
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
<img src={require('./img/search-ex-1.png').default} width="70%" title='Search Example 1' />
</TabItem>
<TabItem value="Mobile" label="Mobile">
<img src={require('./img/moblie-smart-serach.webp').default} width="30%" title='Smart search on mobile' />
</TabItem>
</Tabs>
+1 -1
View File
@@ -4,7 +4,7 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
- Copy the following `docker-compose.yml` to your ML system.
- Start the container by running `docker compose up -d`.
- Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version).
:::note Info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
-4
View File
@@ -11,10 +11,6 @@ Hardware and software requirements for Immich
- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose](https://docs.docker.com/compose/install/)
:::note
Immich requires the command `docker compose` - the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer compatible with Immich.
:::
:::info Podman
You can also use Podman to run the application. However, additional configuration might be required.
:::
+3 -2
View File
@@ -17,11 +17,12 @@ curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | b
The script will perform the following actions:
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
2. Start the containers.
2. Populate the `.env` file with necessary information based on the current directory path.
3. Start the containers.
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`
The directory which is used to store the library files is `./immich-app` relative to the current directory.
The directory which is used to store the library files is `./immich-data` relative to the current directory.
:::tip
For common next steps, see [Post Install Steps](/docs/install/post-install.mdx).
+1 -1
View File
@@ -98,7 +98,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
> Note: This can take several minutes depending on your Internet speed and Unraid hardware
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_server` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_web` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
<img
src={require('./img/unraid06.webp').default}
-1
View File
@@ -25,4 +25,3 @@
/docs/developer/contributing /docs/developer/pr-checklist 301
/docs/guides/machine-learning /docs/guides/remote-machine-learning 301
/docs/administration/password-login /docs/administration/system-settings 301
/docs/features/search /docs/features/smart-search 301
+32 -48
View File
@@ -1,78 +1,62 @@
#!/usr/bin/env bash
set -o nounset
set -o pipefail
create_immich_directory() { local -r Tgt='./immich-app'
echo "Starting Immich installation..."
ip_address=$(hostname -I | awk '{print $1}')
create_immich_directory() {
echo "Creating Immich directory..."
if [[ -e $Tgt ]]; then
echo "Found existing directory $Tgt, will overwrite YAML files"
else
mkdir "$Tgt" || return
fi
cd "$Tgt" || return
mkdir -p ./immich-app
cd ./immich-app || exit
}
download_docker_compose_file() {
echo "Downloading docker-compose.yml..."
"${Curl[@]}" "$RepoUrl"/docker-compose.yml -o ./docker-compose.yml
curl -L https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml -o ./docker-compose.yml >/dev/null 2>&1
}
download_dot_env_file() {
echo "Downloading .env file..."
"${Curl[@]}" "$RepoUrl"/example.env -o ./.env
curl -L https://github.com/immich-app/immich/releases/latest/download/example.env -o ./.env >/dev/null 2>&1
}
start_docker_compose() {
echo "Starting Immich's docker containers"
if ! docker compose >/dev/null 2>&1; then
echo "failed to find 'docker compose'"
return 1
if docker compose >/dev/null 2>&1; then
docker_bin="docker compose"
elif docker-compose >/dev/null 2>&1; then
docker_bin="docker-compose"
else
echo "Cannot find \`docker compose\` or \`docker-compose\`."
exit 1
fi
if ! docker compose up --remove-orphans -d; then
if $docker_bin up --remove-orphans -d; then
show_friendly_message
exit 0
else
echo "Could not start. Check for errors above."
return 1
exit 1
fi
show_friendly_message
}
show_friendly_message() {
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
cat << EOF
Successfully deployed Immich!
You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api
---------------------------------------------------
If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
echo "Successfully deployed Immich!"
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
echo "---------------------------------------------------"
echo "If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
1. First bring down the containers with the command 'docker compose down' in the immich-app directory,
1. First bring down the containers with the command 'docker-compose down' in the immich-app directory,
2. Then change the information that fits your needs in the '.env' file,
3. Finally, bring the containers back up with the command 'docker compose up --remove-orphans -d' in the immich-app directory
EOF
3. Finally, bring the containers back up with the command 'docker-compose up --remove-orphans -d' in the immich-app directory"
}
# MAIN
main() {
echo "Starting Immich installation..."
local -r RepoUrl='https://github.com/immich-app/immich/releases/latest/download'
local -a Curl
if command -v curl >/dev/null; then
Curl=(curl -fsSL)
else
echo 'no curl binary found; please install curl and try again'
return 14
fi
create_immich_directory || { echo 'error creating Immich directory'; return 10; }
download_docker_compose_file || { echo 'error downloading Docker Compose file'; return 11; }
download_dot_env_file || { echo 'error downloading .env'; return 12; }
start_docker_compose || { echo 'error starting Docker'; return 13; }
return 0; }
main
Exit=$?
[[ $Exit == 0 ]] || echo "There was an error installing Immich. Exit code: $Exit. Please provide these logs when asking for assistance."
exit "$Exit"
create_immich_directory
download_docker_compose_file
download_dot_env_file
start_docker_compose
+2 -2
View File
@@ -1,6 +1,6 @@
ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:b4b901d9304f0b4af0157eb021300eb4775a3af8fdbd3e9504dbfd4be16e9ee3 as builder-cpu
FROM python:3.11-bookworm@sha256:e2ed446c899827ed992f8a5a8875fa0853fcab32581e61418b650322061aa3c4 as builder-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
USER root
@@ -36,7 +36,7 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:3800945e7ed50341ba8af48f449515c0a4e845277d56008c15bd84d52093e958 as prod-cpu
FROM python:3.11-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as prod-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
USER root
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:987299cff637df33d00a9969a577f2ac74d37fa070db6aa45b083346ba390959 as builder
FROM mambaorg/micromamba:bookworm-slim@sha256:3624db3aee11d2f3f00d25f691aaaf8834b8bc4ec1b340dcdb48ef37281ea604 as builder
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \
+62 -62
View File
@@ -680,18 +680,18 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.110.1"
version = "0.110.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.110.1-py3-none-any.whl", hash = "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc"},
{file = "fastapi-0.110.1.tar.gz", hash = "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad"},
{file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"},
{file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.37.2,<0.38.0"
starlette = ">=0.36.3,<0.37.0"
typing-extensions = ">=4.8.0"
[package.extras]
@@ -2383,47 +2383,47 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.15"
version = "1.10.14"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"},
{file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"},
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"},
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"},
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"},
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"},
{file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"},
{file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"},
{file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"},
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"},
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"},
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"},
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"},
{file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"},
{file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"},
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"},
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"},
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"},
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"},
{file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"},
{file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"},
{file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"},
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"},
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"},
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"},
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"},
{file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"},
{file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"},
{file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"},
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"},
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"},
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"},
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"},
{file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"},
{file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"},
{file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"},
{file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"},
{file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"},
{file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"},
{file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"},
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"},
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"},
{file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"},
{file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"},
{file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"},
{file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"},
{file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"},
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"},
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"},
{file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"},
{file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"},
{file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"},
{file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"},
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"},
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"},
{file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"},
{file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"},
{file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"},
{file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"},
{file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"},
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"},
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"},
{file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"},
{file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"},
{file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"},
{file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"},
{file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"},
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"},
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"},
{file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"},
{file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"},
{file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"},
]
[package.dependencies]
@@ -2846,28 +2846,28 @@ files = [
[[package]]
name = "ruff"
version = "0.3.5"
version = "0.3.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac"},
{file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1"},
{file = "ruff-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936"},
{file = "ruff-0.3.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985"},
{file = "ruff-0.3.5-py3-none-win32.whl", hash = "sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d"},
{file = "ruff-0.3.5-py3-none-win_amd64.whl", hash = "sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e"},
{file = "ruff-0.3.5-py3-none-win_arm64.whl", hash = "sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"},
{file = "ruff-0.3.5.tar.gz", hash = "sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d"},
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
{file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
{file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
{file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
{file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
]
[[package]]
@@ -3047,13 +3047,13 @@ files = [
[[package]]
name = "starlette"
version = "0.37.2"
version = "0.36.3"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
files = [
{file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"},
{file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"},
{file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"},
{file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"},
]
[package.dependencies]
+1
View File
@@ -11,6 +11,7 @@ Name | Type | Description | Notes
**exclusionPatterns** | **List<String>** | | [optional] [default to const []]
**importPaths** | **List<String>** | | [optional] [default to const []]
**isVisible** | **bool** | | [optional]
**isWatched** | **bool** | | [optional]
**name** | **String** | | [optional]
**ownerId** | **String** | |
**type** | [**LibraryType**](LibraryType.md) | |
+18 -1
View File
@@ -16,6 +16,7 @@ class CreateLibraryDto {
this.exclusionPatterns = const [],
this.importPaths = const [],
this.isVisible,
this.isWatched,
this.name,
required this.ownerId,
required this.type,
@@ -33,6 +34,14 @@ class CreateLibraryDto {
///
bool? isVisible;
///
/// 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? isWatched;
///
/// 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
@@ -50,6 +59,7 @@ class CreateLibraryDto {
_deepEquality.equals(other.exclusionPatterns, exclusionPatterns) &&
_deepEquality.equals(other.importPaths, importPaths) &&
other.isVisible == isVisible &&
other.isWatched == isWatched &&
other.name == name &&
other.ownerId == ownerId &&
other.type == type;
@@ -60,12 +70,13 @@ class CreateLibraryDto {
(exclusionPatterns.hashCode) +
(importPaths.hashCode) +
(isVisible == null ? 0 : isVisible!.hashCode) +
(isWatched == null ? 0 : isWatched!.hashCode) +
(name == null ? 0 : name!.hashCode) +
(ownerId.hashCode) +
(type.hashCode);
@override
String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, name=$name, ownerId=$ownerId, type=$type]';
String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, isWatched=$isWatched, name=$name, ownerId=$ownerId, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -76,6 +87,11 @@ class CreateLibraryDto {
} else {
// json[r'isVisible'] = null;
}
if (this.isWatched != null) {
json[r'isWatched'] = this.isWatched;
} else {
// json[r'isWatched'] = null;
}
if (this.name != null) {
json[r'name'] = this.name;
} else {
@@ -101,6 +117,7 @@ class CreateLibraryDto {
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false)
: const [],
isVisible: mapValueOfType<bool>(json, r'isVisible'),
isWatched: mapValueOfType<bool>(json, r'isWatched'),
name: mapValueOfType<String>(json, r'name'),
ownerId: mapValueOfType<String>(json, r'ownerId')!,
type: LibraryType.fromJson(json[r'type'])!,
+5
View File
@@ -31,6 +31,11 @@ void main() {
// TODO
});
// bool isWatched
test('to test the property `isWatched`', () async {
// TODO
});
// String name
test('to test the property `name`', () async {
// TODO
+3
View File
@@ -8000,6 +8000,9 @@
"isVisible": {
"type": "boolean"
},
"isWatched": {
"type": "boolean"
},
"name": {
"type": "string"
},
@@ -461,6 +461,7 @@ export type CreateLibraryDto = {
exclusionPatterns?: string[];
importPaths?: string[];
isVisible?: boolean;
isWatched?: boolean;
name?: string;
ownerId: string;
"type": LibraryType;
+1 -1
View File
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
# web build
FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e as web
FROM node:iron-alpine3.18@sha256:fa5d3cf51725bd42d32e67917623038539dbe720dab082f590785c001eb4dfef as web
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+3
View File
@@ -32,6 +32,9 @@ export class CreateLibraryDto {
@ArrayUnique()
@ArrayMaxSize(128)
exclusionPatterns?: string[];
@ValidateBoolean({ optional: true })
isWatched?: boolean;
}
export class UpdateLibraryDto {
@@ -1058,6 +1058,14 @@ describe(LibraryService.name, () => {
expect(libraryMock.create).not.toHaveBeenCalled();
});
it('should not create watched', async () => {
await expect(
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD, isWatched: true }),
).rejects.toBeInstanceOf(BadRequestException);
expect(storageMock.watch).not.toHaveBeenCalled();
});
});
});
+3
View File
@@ -266,6 +266,9 @@ export class LibraryService extends EventEmitter {
if (dto.exclusionPatterns && dto.exclusionPatterns.length > 0) {
throw new BadRequestException('Upload libraries cannot have exclusion patterns');
}
if (dto.isWatched) {
throw new BadRequestException('Upload libraries cannot be watched');
}
break;
}
}
@@ -346,6 +346,19 @@ describe(SystemConfigService.name, () => {
});
});
describe('refreshConfig', () => {
it('should notify the subscribers', async () => {
const changeMock = jest.fn();
const subscription = sut.config$.subscribe(changeMock);
await sut.refreshConfig();
expect(changeMock).toHaveBeenCalledWith(defaults);
subscription.unsubscribe();
});
});
describe('getCustomCss', () => {
it('should return the default theme', async () => {
await expect(sut.getCustomCss()).resolves.toEqual(defaults.theme.customCss);
@@ -90,6 +90,13 @@ export class SystemConfigService {
return mapConfig(newConfig);
}
// this is only used by the cli on config change, and it's not actually needed anymore
async refreshConfig() {
this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null);
await this.core.refreshConfig();
return true;
}
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
const options = new SystemConfigTemplateStorageOptionDto();
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e
FROM node:iron-alpine3.18@sha256:fa5d3cf51725bd42d32e67917623038539dbe720dab082f590785c001eb4dfef
RUN apk add --no-cache tini
USER node
@@ -42,8 +42,7 @@
</script>
<ConfirmDialogue
id="delete-user-confirmation-modal"
title="Delete user"
title="Delete User"
confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
onConfirm={handleDeleteUser}
onClose={() => dispatch('cancel')}
@@ -147,7 +147,6 @@
{#if confirmJob}
<ConfirmDialogue
id="reprocess-faces-modal"
prompt="Are you sure you want to reprocess all faces? This will also clear named people."
{onConfirm}
onClose={() => (confirmJob = null)}
@@ -28,8 +28,7 @@
</script>
<ConfirmDialogue
id="restore-user-modal"
title="Restore user"
title="Restore User"
confirmText="Continue"
confirmColor="green"
onConfirm={handleRestoreUser}
@@ -5,7 +5,7 @@
export let onConfirm: () => void;
</script>
<ConfirmDialogue id="disable-login-modal" title="Disable login" onClose={onCancel} {onConfirm}>
<ConfirmDialogue title="Disable Login" onClose={onCancel} {onConfirm}>
<svelte:fragment slot="prompt">
<div class="flex flex-col gap-4">
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p>
@@ -179,7 +179,7 @@
<SettingSelect
label="TRANSCODE POLICY"
{disabled}
desc="Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled)."
desc="Policy for when a video should be transcoded."
bind:value={config.ffmpeg.transcode}
name="transcode"
options={[
@@ -1,8 +1,10 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk';
import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js';
import { mdiArrowDownThin, mdiArrowUpThin, mdiClose, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
@@ -50,52 +52,67 @@
};
</script>
<FullScreenModal id="album-options-modal" title="Options" onClose={() => dispatch('close')}>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title="Display order"
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
/>
{/if}
<SettingSwitch
id="comments-likes"
title="Comments & likes"
subtitle="Let others respond"
checked={album.isActivityEnabled}
on:toggle={() => dispatch('toggleEnableActivity')}
/>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">PEOPLE</div>
<div class="p-2">
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
<div>Invite People</div>
</button>
<div class="flex items-center gap-2 py-2 mt-2">
<FullScreenModal onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden p-2 md:p-0">
<div
class="w-[550px] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div class="px-2 pt-2">
<div class="flex items-center">
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1>
<div>
<UserAvatar {user} size="md" />
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
</div>
<div class="w-full">{user.name}</div>
<div>Owner</div>
</div>
{#each album.sharedUsers as user (user.id)}
<div class="flex items-center gap-2 py-2">
<div>
<UserAvatar {user} size="md" />
<div class=" items-center justify-center p-4">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title="Display order"
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
/>
{/if}
<SettingSwitch
id="comments-likes"
title="Comments & likes"
subtitle="Let others respond"
checked={album.isActivityEnabled}
on:toggle={() => dispatch('toggleEnableActivity')}
/>
</div>
<div class="w-full">{user.name}</div>
</div>
{/each}
<div class="py-2">
<div class="text-gray text-sm mb-3">PEOPLE</div>
<div class="p-2">
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
<div>Invite People</div>
</button>
<div class="flex items-center gap-2 py-2 mt-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
<div>Owner</div>
</div>
{#each album.sharedUsers as user (user.id)}
<div class="flex items-center gap-2 py-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
</div>
{/each}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -432,7 +432,7 @@
{#if allowEdit}
<!-- Edit Modal -->
{#if albumToEdit}
<FullScreenModal id="edit-album-modal" title="Edit album" width="wide" onClose={() => (albumToEdit = null)}>
<FullScreenModal onClose={() => (albumToEdit = null)}>
<EditAlbumForm album={albumToEdit} onEditSuccess={successEditAlbumInfo} onCancel={() => (albumToEdit = null)} />
</FullScreenModal>
{/if}
@@ -458,8 +458,7 @@
<!-- Delete Modal -->
{#if albumToDelete}
<ConfirmDialogue
id="delete-album-dialogue-modal"
title="Delete album"
title="Delete Album"
confirmText="Delete"
onConfirm={deleteSelectedAlbum}
onClose={() => (albumToDelete = null)}
@@ -121,8 +121,7 @@
{#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id}
<ConfirmDialogue
id="leave-album-modal"
title="Leave album?"
title="Leave Album?"
prompt="Are you sure you want to leave {album.albumName}?"
confirmText="Leave"
onConfirm={handleRemoveUser}
@@ -132,8 +131,7 @@
{#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id}
<ConfirmDialogue
id="remove-user-modal"
title="Remove user?"
title="Remove User?"
prompt="Are you sure you want to remove {selectedRemoveUser.name}"
confirmText="Remove"
onConfirm={handleRemoveUser}
@@ -21,7 +21,7 @@
export let peopleWithFaces: AssetFaceResponseDto[];
export let allPeople: PersonResponseDto[];
export let editedPersonIndex: number;
export let editedPerson: PersonResponseDto;
export let assetType: AssetTypeEnum;
export let assetId: string;
@@ -106,7 +106,7 @@
const handleCreatePerson = async () => {
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id);
const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
const newFeaturePhoto = personToUpdate ? await zoomImageToBase64(personToUpdate) : null;
@@ -229,7 +229,7 @@
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
{#if searchName == ''}
{#each allPeople as person (person.id)}
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id}
{#if person.id !== editedPerson.id}
<div class="w-fit">
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
<div class="relative">
@@ -255,7 +255,7 @@
{/each}
{:else}
{#each searchedPeople as person (person.id)}
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id}
{#if person.id !== editedPerson.id}
<div class="w-fit">
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
<div class="relative">
@@ -161,7 +161,6 @@
{#if isShowConfirmation}
<ConfirmDialogue
id="merge-people-modal"
title="Merge people"
confirmText="Merge"
onConfirm={handleMerge}
@@ -3,7 +3,7 @@
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeft, mdiMerge } from '@mdi/js';
import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Button from '../elements/buttons/button.svelte';
@@ -30,80 +30,95 @@
};
</script>
<FullScreenModal id="merge-people-modal" title="Merge people - {title}" onClose={() => dispatch('close')}>
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
{#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(personMerge1.id)}
altText={personMerge1.name}
widthStyle="100%"
/>
</div>
<div class="mx-0.5 flex md:mx-2">
<CircleIconButton
title="Swap merge direction"
icon={mdiMerge}
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
/>
<FullScreenModal onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div
class="w-[250px] max-w-[125vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg md:w-[375px]"
>
<div class="relative flex items-center justify-between">
<h1 class="truncate px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">
Merge People - {title}
</h1>
<div class="p-2">
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
</div>
</div>
<button
disabled={potentialMergePeople.length === 0}
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
on:click={() => {
if (potentialMergePeople.length > 0) {
choosePersonToMerge = !choosePersonToMerge;
}
}}
>
<ImageThumbnail
border={potentialMergePeople.length > 0}
circle
shadow
url={getPeopleThumbnailUrl(personMerge2.id)}
altText={personMerge2.name}
widthStyle="100%"
/>
</button>
{:else}
<div class="grid w-full grid-cols-1 gap-2">
<div class="px-2">
<button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
</div>
<div class="flex items-center justify-center">
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
{#each potentialMergePeople as person (person.id)}
<div class="h-24 w-24 md:h-28 md:w-28">
<button class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
<ImageThumbnail
border={true}
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
on:click={() => changePersonToMerge(person)}
/>
</button>
</div>
{/each}
<div class="flex items-center justify-center px-2 py-4 md:h-36 md:px-4 md:py-4">
{#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(personMerge1.id)}
altText={personMerge1.name}
widthStyle="100%"
/>
</div>
<div class="mx-0.5 flex md:mx-2">
<CircleIconButton
title="Swap merge direction"
icon={mdiMerge}
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
/>
</div>
</div>
</div>
{/if}
</div>
<div class="flex px-4 md:pt-4">
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
</div>
<div class="flex px-4 pt-2">
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
</div>
<div class="mt-8 flex w-full gap-4 pb-4">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
<button
disabled={potentialMergePeople.length === 0}
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
on:click={() => {
if (potentialMergePeople.length > 0) {
choosePersonToMerge = !choosePersonToMerge;
}
}}
>
<ImageThumbnail
border={potentialMergePeople.length > 0}
circle
shadow
url={getPeopleThumbnailUrl(personMerge2.id)}
altText={personMerge2.name}
widthStyle="100%"
/>
</button>
{:else}
<div class="grid w-full grid-cols-1 gap-2">
<div class="px-2">
<button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
</div>
<div class="flex items-center justify-center">
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
{#each potentialMergePeople as person (person.id)}
<div class="h-24 w-24 md:h-28 md:w-28">
<button class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
<ImageThumbnail
border={true}
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
on:click={() => changePersonToMerge(person)}
/>
</button>
</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
<div class="flex px-4 md:px-8 md:pt-4">
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
</div>
<div class="flex px-4 pt-2 md:px-8">
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
</div>
<div class="mt-8 flex w-full gap-4 px-4 pb-4">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
</div>
</div>
</div>
</FullScreenModal>
@@ -28,14 +28,14 @@
export let assetType: AssetTypeEnum;
// keep track of the changes
let numberOfPersonToCreate: string[] = [];
let numberOfAssetFaceGenerated: string[] = [];
let peopleToCreate: string[] = [];
let assetFaceGenerated: string[] = [];
// faces
let peopleWithFaces: AssetFaceResponseDto[] = [];
let selectedPersonToReassign: (PersonResponseDto | null)[];
let selectedPersonToCreate: (string | null)[];
let editedPersonIndex: number;
let selectedPersonToReassign: Record<string, PersonResponseDto> = {};
let selectedPersonToCreate: Record<string, string> = {};
let editedPerson: PersonResponseDto;
// loading spinners
let isShowLoadingDone = false;
@@ -49,6 +49,8 @@
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
let automaticRefreshTimeout: ReturnType<typeof setTimeout>;
const thumbnailWidth = '90px';
const dispatch = createEventDispatcher<{
close: void;
refresh: void;
@@ -60,8 +62,6 @@
const { people } = await getAllPeople({ withHidden: true });
allPeople = people;
peopleWithFaces = await getFaces({ id: assetId });
selectedPersonToCreate = Array.from({ length: peopleWithFaces.length });
selectedPersonToReassign = Array.from({ length: peopleWithFaces.length });
} catch (error) {
handleError(error, "Can't get faces");
} finally {
@@ -71,12 +71,12 @@
}
const onPersonThumbnail = (personId: string) => {
numberOfAssetFaceGenerated.push(personId);
assetFaceGenerated.push(personId);
if (
isEqual(numberOfAssetFaceGenerated, numberOfPersonToCreate) &&
isEqual(assetFaceGenerated, peopleToCreate) &&
loaderLoadingDoneTimeout &&
automaticRefreshTimeout &&
selectedPersonToCreate.filter((person) => person !== null).length === numberOfPersonToCreate.length
Object.keys(selectedPersonToCreate).length === peopleToCreate.length
) {
clearTimeout(loaderLoadingDoneTimeout);
clearTimeout(automaticRefreshTimeout);
@@ -97,36 +97,41 @@
dispatch('close');
};
const handleReset = (index: number) => {
if (selectedPersonToReassign[index]) {
selectedPersonToReassign[index] = null;
const handleReset = (id: string) => {
if (selectedPersonToReassign[id]) {
delete selectedPersonToReassign[id];
// trigger reactivity
selectedPersonToReassign = selectedPersonToReassign;
}
if (selectedPersonToCreate[index]) {
selectedPersonToCreate[index] = null;
if (selectedPersonToCreate[id]) {
delete selectedPersonToCreate[id];
// trigger reactivity
selectedPersonToCreate = selectedPersonToCreate;
}
};
const handleEditFaces = async () => {
loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), timeBeforeShowLoadingSpinner);
const numberOfChanges =
selectedPersonToCreate.filter((person) => person !== null).length +
selectedPersonToReassign.filter((person) => person !== null).length;
const numberOfChanges = Object.keys(selectedPersonToCreate).length + Object.keys(selectedPersonToReassign).length;
if (numberOfChanges > 0) {
try {
for (const [index, peopleWithFace] of peopleWithFaces.entries()) {
const personId = selectedPersonToReassign[index]?.id;
for (const personWithFace of peopleWithFaces) {
const personId = selectedPersonToReassign[personWithFace.id]?.id;
if (personId) {
await reassignFacesById({
id: personId,
faceDto: { id: peopleWithFace.id },
faceDto: { id: personWithFace.id },
});
} else if (selectedPersonToCreate[index]) {
} else if (selectedPersonToCreate[personWithFace.id]) {
const data = await createPerson({ personCreateDto: {} });
numberOfPersonToCreate.push(data.id);
peopleToCreate.push(data.id);
await reassignFacesById({
id: data.id,
faceDto: { id: peopleWithFace.id },
faceDto: { id: personWithFace.id },
});
}
}
@@ -141,7 +146,7 @@
}
isShowLoadingDone = false;
if (numberOfPersonToCreate.length === 0) {
if (peopleToCreate.length === 0) {
clearTimeout(loaderLoadingDoneTimeout);
dispatch('refresh');
} else {
@@ -150,23 +155,26 @@
};
const handleCreatePerson = (newFeaturePhoto: string | null) => {
const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id);
const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
if (newFeaturePhoto && personToUpdate) {
selectedPersonToCreate[peopleWithFaces.indexOf(personToUpdate)] = newFeaturePhoto;
selectedPersonToCreate[personToUpdate.id] = newFeaturePhoto;
}
showSeletecFaces = false;
};
const handleReassignFace = (person: PersonResponseDto | null) => {
if (person) {
selectedPersonToReassign[editedPersonIndex] = person;
const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
if (person && personToUpdate) {
selectedPersonToReassign[personToUpdate.id] = person;
showSeletecFaces = false;
}
};
const handlePersonPicker = (index: number) => {
editedPersonIndex = index;
showSeletecFaces = true;
const handlePersonPicker = (person: PersonResponseDto | null) => {
if (person) {
editedPerson = person;
showSeletecFaces = true;
}
};
</script>
@@ -217,35 +225,48 @@
on:mouseleave={() => ($boundingBoxesArray = [])}
>
<div class="relative">
<ImageThumbnail
curve
shadow
url={selectedPersonToCreate[index] ||
getPeopleThumbnailUrl(selectedPersonToReassign[index]?.id || face.person.id)}
altText={selectedPersonToReassign[index]
? selectedPersonToReassign[index]?.name
: selectedPersonToCreate[index]
? 'New person'
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
title={selectedPersonToReassign[index]
? selectedPersonToReassign[index]?.name
: selectedPersonToCreate[index]
? 'New person'
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
hidden={selectedPersonToReassign[index]
? selectedPersonToReassign[index]?.isHidden
: selectedPersonToCreate[index]
? false
: face.person?.isHidden}
/>
{#if selectedPersonToCreate[face.id]}
<ImageThumbnail
curve
shadow
url={selectedPersonToCreate[face.id]}
altText={selectedPersonToCreate[face.id]}
title={'New person'}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
/>
{:else if selectedPersonToReassign[face.id]}
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id].id)}
altText={selectedPersonToReassign[face.id]?.name || selectedPersonToReassign[face.id].id}
title={getPersonNameWithHiddenValue(
selectedPersonToReassign[face.id].name,
face.person?.isHidden,
)}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
hidden={selectedPersonToReassign[face.id].isHidden}
/>
{:else}
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(face.person.id)}
altText={face.person.name || face.person.id}
title={getPersonNameWithHiddenValue(face.person.name, face.person.isHidden)}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
hidden={face.person.isHidden}
/>
{/if}
</div>
{#if !selectedPersonToCreate[index]}
{#if !selectedPersonToCreate[face.id]}
<p class="relative mt-1 truncate font-medium" title={face.person?.name}>
{#if selectedPersonToReassign[index]?.id}
{selectedPersonToReassign[index]?.name}
{#if selectedPersonToReassign[face.id]?.id}
{selectedPersonToReassign[face.id]?.name}
{:else}
{face.person?.name}
{/if}
@@ -253,8 +274,8 @@
{/if}
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700">
{#if selectedPersonToCreate[index] || selectedPersonToReassign[index]}
<button on:click={() => handleReset(index)} class="flex h-full w-full">
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
<button on:click={() => handleReset(face.id)} class="flex h-full w-full">
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<div>
<Icon path={mdiRestart} size={18} />
@@ -262,7 +283,7 @@
</div>
</button>
{:else}
<button on:click={() => handlePersonPicker(index)} class="flex h-full w-full">
<button on:click={() => handlePersonPicker(face.person)} class="flex h-full w-full">
<div
class="absolute left-1/2 top-1/2 h-[2px] w-[14px] translate-x-[-50%] translate-y-[-50%] transform bg-white"
/>
@@ -282,7 +303,7 @@
<AssignFaceSidePanel
{peopleWithFaces}
{allPeople}
{editedPersonIndex}
{editedPerson}
{assetType}
{assetId}
on:close={() => (showSeletecFaces = false)}
@@ -3,6 +3,7 @@
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiCake } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import DateInput from '../elements/date-input.svelte';
export let birthDate: string;
@@ -20,27 +21,36 @@
};
</script>
<FullScreenModal id="set-birthday-modal" title="Set date of birth" icon={mdiCake} onClose={handleCancel}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg">
Date of birth is used to calculate the age of this person at the time of a photo.
</p>
</div>
<FullScreenModal onClose={handleCancel}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiCake} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Set date of birth</h1>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<div class="my-4 flex flex-col gap-2">
<DateInput
class="immich-form-input"
id="birthDate"
name="birthDate"
type="date"
bind:value={birthDate}
max={todayFormatted}
/>
<p class="text-sm dark:text-immich-dark-fg">
Date of birth is used to calculate the age of this person at the time of a photo.
</p>
</div>
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Set</Button>
</div>
</form>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<DateInput
class="immich-form-input"
id="birthDate"
name="birthDate"
type="date"
bind:value={birthDate}
max={todayFormatted}
/>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Set</Button>
</div>
</form>
</div>
</FullScreenModal>
@@ -1,4 +1,5 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import type { ApiKeyResponseDto } from '@immich/sdk';
import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
@@ -7,7 +8,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification';
export let apiKey: Partial<ApiKeyResponseDto>;
export let title: string;
export let title = 'API Key';
export let cancelText = 'Cancel';
export let submitText = 'Save';
@@ -28,16 +29,29 @@
};
</script>
<FullScreenModal id="api-key-modal" {title} icon={mdiKeyVariant} onClose={handleCancel}>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="mb-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
<FullScreenModal onClose={handleCancel}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiKeyVariant} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h1>
</div>
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button>
<Button type="submit" fullwidth>{submitText}</Button>
</div>
</form>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button>
<Button type="submit" fullwidth>{submitText}</Button>
</div>
</form>
</div>
</FullScreenModal>
@@ -1,4 +1,5 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { copyToClipboard } from '$lib/utils';
import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
@@ -19,22 +20,31 @@
});
</script>
<FullScreenModal id="api-key-secret-modal" title="API key" icon={mdiKeyVariant} onClose={() => handleDone()}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg">
This value will only be shown once. Please be sure to copy it before closing the window.
</p>
</div>
<FullScreenModal>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiKeyVariant} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">API Key</h1>
<div class="my-4 flex flex-col gap-2">
<!-- <label class="immich-form-label" for="secret">API Key</label> -->
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
</div>
<p class="text-sm dark:text-immich-dark-fg">
This value will only be shown once. Please be sure to copy it before closing the window.
</p>
</div>
<div class="mt-8 flex w-full gap-4">
{#if canCopyImagesToClipboard}
<Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
{/if}
<Button on:click={() => handleDone()} fullwidth>Done</Button>
<div class="m-4 flex flex-col gap-2">
<!-- <label class="immich-form-label" for="secret">API Key</label> -->
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
</div>
<div class="mt-8 flex w-full gap-4 px-4">
{#if canCopyImagesToClipboard}
<Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
{/if}
<Button on:click={() => handleDone()} fullwidth>Done</Button>
</div>
</div>
</FullScreenModal>
@@ -5,6 +5,7 @@
import { createUser } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
import PasswordField from '../shared-components/password-field.svelte';
import Slider from '../elements/slider.svelte';
@@ -68,53 +69,62 @@
}
</script>
<form on:submit|preventDefault={registerUser} autocomplete="off">
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" bind:value={email} type="email" required />
<div
class="max-h-screen w-[500px] max-w-[95vw] overflow-y-auto immich-scrollbar rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div class="flex flex-col place-content-center place-items-center gap-4 px-4">
<ImmichLogo noText class="text-center" height="75" width="75" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Create new user</h1>
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label>
<PasswordField id="password" bind:password autocomplete="new-password" />
</div>
<form on:submit|preventDefault={registerUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" bind:value={email} type="email" required />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label>
<PasswordField id="password" bind:password autocomplete="new-password" />
</div>
<div class="my-4 flex place-items-center justify-between gap-2">
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
Require user to change password on first login
</label>
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" bind:value={name} type="text" required />
</div>
<div class="m-4 flex place-items-center justify-between gap-2">
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
Require user to change password on first login
</label>
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
Quota Size (GiB)
{#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}
</label>
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" bind:value={name} type="text" required />
</div>
{#if error}
<p class="text-sm text-red-400">{error}</p>
{/if}
<div class="m-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
Quota Size (GiB)
{#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}
</label>
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
</div>
{#if success}
<p class="text-sm text-immich-primary">{success}</p>
{/if}
<div class="flex w-full gap-4 pt-4">
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
</div>
</form>
{#if error}
<p class="ml-4 text-sm text-red-400">{error}</p>
{/if}
{#if success}
<p class="ml-4 text-sm text-immich-primary">{success}</p>
{/if}
<div class="flex w-full gap-4 p-4">
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
</div>
</form>
</div>
@@ -34,29 +34,39 @@
};
</script>
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" />
</div>
<div class="flex-grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">Description</label>
<textarea class="immich-form-input" id="description" bind:value={description} />
</div>
</div>
<div
class="max-h-screen w-[700px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 mb-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit Album</h1>
</div>
<div class="flex justify-center">
<div class="mt-8 flex w-full sm:w-2/3 gap-4">
<Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
<Button type="submit" fullwidth disabled={isSubmitting}>OK</Button>
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" />
</div>
<div class="flex-grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">Description</label>
<textarea class="immich-form-input" id="description" bind:value={description} />
</div>
</div>
</div>
</div>
</form>
<div class="flex justify-center">
<div class="mt-8 flex w-full sm:w-2/3 gap-4 px-4">
<Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
<Button type="submit" fullwidth disabled={isSubmitting}>OK</Button>
</div>
</div>
</form>
</div>
@@ -1,12 +1,16 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import { AppRoute } from '$lib/constants';
import { serverInfo } from '$lib/stores/server-info.store';
import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
import { handleError } from '$lib/utils/handle-error';
import { updateUser, type UserResponseDto } from '@immich/sdk';
import { mdiAccountEditOutline, mdiClose } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
export let user: UserResponseDto;
export let canResetPassword = true;
@@ -87,64 +91,82 @@
}
</script>
<form on:submit|preventDefault={editUser} autocomplete="off">
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
</div>
<FocusTrap>
<div
class="relative max-h-screen w-[500px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div class="absolute top-0 right-0 px-2 py-2 h-fit">
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"
>Quota Size (GiB) {#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}</label
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
<p>Note: Enter 0 for unlimited quota</p>
<Icon path={mdiAccountEditOutline} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit user</h1>
</div>
<form on:submit|preventDefault={editUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"
>Quota Size (GiB) {#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}</label
>
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
<p>Note: Enter 0 for unlimited quota</p>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="storage-label">Storage Label</label>
<input
class="immich-form-input"
id="storage-label"
name="storage-label"
type="text"
bind:value={user.storageLabel}
/>
<p>
Note: To apply the Storage Label to previously uploaded assets, run the
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
Storage Migration Job</a
>
</p>
</div>
{#if error}
<p class="ml-4 text-sm text-red-400">{error}</p>
{/if}
{#if success}
<p class="ml-4 text-sm text-immich-primary">{success}</p>
{/if}
<div class="mt-8 flex w-full gap-4 px-4">
{#if canResetPassword}
<Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
>Reset password</Button
>
{/if}
<Button type="submit" fullwidth>Confirm</Button>
</div>
</form>
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="storage-label">Storage Label</label>
<input
class="immich-form-input"
id="storage-label"
name="storage-label"
type="text"
bind:value={user.storageLabel}
/>
<p>
Note: To apply the Storage Label to previously uploaded assets, run the
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"> Storage Migration Job</a>
</p>
</div>
{#if error}
<p class="ml-4 text-sm text-red-400">{error}</p>
{/if}
{#if success}
<p class="ml-4 text-sm text-immich-primary">{success}</p>
{/if}
<div class="mt-8 flex w-full gap-4">
{#if canResetPassword}
<Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
>Reset password</Button
>
{/if}
<Button type="submit" fullwidth>Confirm</Button>
</div>
</form>
</FocusTrap>
{#if isShowResetPasswordConfirmation}
<ConfirmDialogue
id="reset-password-modal"
title="Reset password"
title="Reset Password"
confirmText="Reset"
onConfirm={resetPassword}
onClose={() => (isShowResetPasswordConfirmation = false)}
@@ -2,6 +2,7 @@
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiFolderRemove } from '@mdi/js';
import { onMount } from 'svelte';
@@ -28,42 +29,48 @@
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
</script>
<FullScreenModal
id="add-exclusion-pattern-modal"
title="Add exclusion pattern"
icon={mdiFolderRemove}
onClose={handleCancel}
>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="py-5 text-sm">
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
folders that contain files you don't want to import, such as RAW files.
<br /><br />
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw",
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore".
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">Pattern</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
<FullScreenModal onClose={handleCancel}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiFolderRemove} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Add Exclusion pattern</h1>
</div>
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
{/if}
</div>
</form>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="p-5 text-sm">
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
folders that contain files you don't want to import, such as RAW files.
<br /><br />
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw",
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore".
</p>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">Pattern</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
{/if}
</div>
</form>
</div>
</FullScreenModal>
@@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Icon from '$lib/components/elements/icon.svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderSync } from '@mdi/js';
@@ -30,30 +31,45 @@
const handleSubmit = () => dispatch('submit', { importPath });
</script>
<FullScreenModal id="library-import-path-modal" {title} icon={mdiFolderSync} onClose={handleCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="py-5 text-sm">
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos.
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">Path</label>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
<FullScreenModal onClose={handleCancel}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiFolderSync} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h1>
</div>
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="p-5 text-sm">
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos. Note that
you are only allowed to import paths inside of your account's external path, configured in the administrative
settings.
</p>
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">Path</label>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">This import path already exists.</p>
{/if}
</div>
</form>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">This import path already exists.</p>
{/if}
</div>
</form>
</div>
</FullScreenModal>
@@ -152,7 +152,7 @@
{#if addImportPath}
<LibraryImportPathForm
title="Add import path"
title="Add Import Path"
submitText="Add"
bind:importPath={importPathToAdd}
{importPaths}
@@ -166,7 +166,7 @@
{#if editImportPath != undefined}
<LibraryImportPathForm
title="Edit import path"
title="Edit Import Path"
submitText="Save"
isEditing={true}
bind:importPath={editedImportPath}
@@ -169,7 +169,7 @@
size="sm"
on:click={() => {
addExclusionPattern = true;
}}>Add exclusion pattern</Button
}}>Add Exclusion Pattern</Button
></td
></tr
>
@@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Icon from '$lib/components/elements/icon.svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderSync } from '@mdi/js';
@@ -27,21 +28,27 @@
const handleSubmit = () => dispatch('submit', { ownerId });
</script>
<FullScreenModal
id="select-library-owner-modal"
title="Select library owner"
icon={mdiFolderSync}
onClose={handleCancel}
>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Create</Button>
<FullScreenModal onClose={handleCancel}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiFolderSync} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Select library owner</h1>
</div>
</form>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Create</Button>
</div>
</form>
</div>
</FullScreenModal>
@@ -21,92 +21,98 @@
const handleClose = () => dispatch('close');
</script>
<FullScreenModal id="map-settings-modal" title="Map settings" onClose={handleClose}>
<form
on:submit|preventDefault={() => dispatch('save', settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
<FullScreenModal onClose={handleClose}>
<div
class="flex w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
>
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
<DateInput
class="immich-form-input w-40"
type="date"
id="date-after"
max={settings.dateBefore}
bind:value={settings.dateAfter}
/>
</div>
<div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-before">Date before</label>
<DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
</div>
<div class="flex justify-center text-xs">
<LinkButton
on:click={() => {
customDateRange = false;
settings.dateAfter = '';
settings.dateBefore = '';
}}
>
Remove custom date range
</LinkButton>
</div>
</div>
{:else}
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
<SettingSelect
label="Date range"
name="date-range"
bind:value={settings.relativeDate}
options={[
{
value: '',
text: 'All',
},
{
value: Duration.fromObject({ hours: 24 }).toISO() || '',
text: 'Past 24 hours',
},
{
value: Duration.fromObject({ days: 7 }).toISO() || '',
text: 'Past 7 days',
},
{
value: Duration.fromObject({ days: 30 }).toISO() || '',
text: 'Past 30 days',
},
{
value: Duration.fromObject({ years: 1 }).toISO() || '',
text: 'Past year',
},
{
value: Duration.fromObject({ years: 3 }).toISO() || '',
text: 'Past 3 years',
},
]}
/>
<div class="text-xs">
<LinkButton
on:click={() => {
customDateRange = true;
settings.relativeDate = '';
}}
>
Use custom date range instead
</LinkButton>
</div>
</div>
{/if}
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Map Settings</h1>
<div class="mt-4 flex w-full gap-4">
<Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button>
<Button type="submit" size="sm" fullwidth>Save</Button>
</div>
</form>
<form
on:submit|preventDefault={() => dispatch('save', settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
>
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
<DateInput
class="immich-form-input w-40"
type="date"
id="date-after"
max={settings.dateBefore}
bind:value={settings.dateAfter}
/>
</div>
<div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-before">Date before</label>
<DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
</div>
<div class="flex justify-center text-xs">
<LinkButton
on:click={() => {
customDateRange = false;
settings.dateAfter = '';
settings.dateBefore = '';
}}
>
Remove custom date range
</LinkButton>
</div>
</div>
{:else}
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
<SettingSelect
label="Date range"
name="date-range"
bind:value={settings.relativeDate}
options={[
{
value: '',
text: 'All',
},
{
value: Duration.fromObject({ hours: 24 }).toISO() || '',
text: 'Past 24 hours',
},
{
value: Duration.fromObject({ days: 7 }).toISO() || '',
text: 'Past 7 days',
},
{
value: Duration.fromObject({ days: 30 }).toISO() || '',
text: 'Past 30 days',
},
{
value: Duration.fromObject({ years: 1 }).toISO() || '',
text: 'Past year',
},
{
value: Duration.fromObject({ years: 3 }).toISO() || '',
text: 'Past 3 years',
},
]}
/>
<div class="text-xs">
<LinkButton
on:click={() => {
customDateRange = true;
settings.relativeDate = '';
}}
>
Use custom date range instead
</LinkButton>
</div>
</div>
{/if}
<div class="mt-4 flex w-full gap-4">
<Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button>
<Button type="submit" size="sm" fullwidth>Save</Button>
</div>
</form>
</div>
</FullScreenModal>
@@ -57,7 +57,6 @@
{#if isShowConfirmation}
<ConfirmDialogue
id="remove-from-album-modal"
title="Remove from {album.albumName}"
confirmText="Remove"
onConfirm={removeFromAlbum}
@@ -50,8 +50,7 @@
{#if removing}
<ConfirmDialogue
id="remove-assets-modal"
title="Remove assets?"
title="Remove Assets?"
prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?"
confirmText="Remove"
onConfirm={() => handleRemove()}
@@ -25,8 +25,7 @@
</script>
<ConfirmDialogue
id="permanently-delete-asset-modal"
title="Permanently delete asset{size > 1 ? 's' : ''}"
title="Permanently Delete Asset{size > 1 ? 's' : ''}"
confirmText="Delete"
onConfirm={handleConfirm}
onClose={() => dispatch('cancel')}
@@ -3,9 +3,12 @@
import { quintOut } from 'svelte/easing';
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { clickOutside } from '$lib/utils/click-outside';
import { mdiClose } from '@mdi/js';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import Icon from '$lib/components/elements/icon.svelte';
const dispatch = createEventDispatcher<{
close: void;
@@ -25,8 +28,6 @@
*/
export let icon: string | undefined = undefined;
$: titleId = `${id}-title`;
onMount(() => {
if (browser) {
const scrollTop = document.documentElement.scrollTop;
@@ -50,7 +51,7 @@
<FocusTrap>
<div
aria-modal="true"
aria-labelledby={titleId}
aria-labelledby={`${id}-title`}
style:z-index={zIndex}
transition:fade={{ duration: 100, easing: quintOut }}
class="fixed left-0 top-0 flex h-full w-full place-content-center place-items-center overflow-hidden bg-black/50"
@@ -60,11 +61,23 @@
onOutclick: () => dispatch('close'),
onEscape: () => dispatch('close'),
}}
class="min-h-[200px] w-[450px] overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar scroll-pb-20"
style="max-height: min(95vh, 800px);"
class="max-h-[800px] min-h-[200px] w-[450px] overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
tabindex="-1"
>
<ModalHeader id={titleId} {title} {showLogo} {icon} on:close />
<div class="flex place-items-center justify-between px-5 py-3">
<div class="flex gap-2 place-items-center">
{#if showLogo}
<ImmichLogo noText={true} width={32} />
{:else if icon}
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
{/if}
<h1 id={`${id}-title`}>
{title}
</h1>
</div>
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} title="Close" />
</div>
<div>
<slot />
@@ -63,16 +63,16 @@
<div role="presentation" on:keydown={handleKeydown}>
<ConfirmDialogue
id="edit-date-time-modal"
confirmColor="primary"
cancelColor="secondary"
title="Edit date and time"
title="Edit date & time"
prompt="Please select a new date:"
disabled={!date.isValid}
onConfirm={handleConfirm}
onClose={handleCancel}
>
<div class="flex flex-col text-md px-4 text-center gap-2" slot="prompt">
<div class="mt-2" />
<div class="flex flex-col">
<label for="datetime">Date and Time</label>
<DateInput
@@ -12,6 +12,7 @@
import SearchBar from '../elements/search-bar.svelte';
import { listNavigation } from '$lib/utils/list-navigation';
export const title = 'Change Location';
export let asset: AssetResponseDto | undefined = undefined;
interface Point {
@@ -94,11 +95,10 @@
</script>
<ConfirmDialogue
id="change-location-modal"
confirmColor="primary"
cancelColor="secondary"
title="Change location"
width="wide"
title="Change Location"
width={800}
onConfirm={handleConfirm}
onClose={handleCancel}
>
@@ -3,7 +3,6 @@
import Button from '../elements/buttons/button.svelte';
import type { Color } from '$lib/components/elements/buttons/button.svelte';
export let id: string;
export let title = 'Confirm';
export let prompt = 'Are you sure you want to do this?';
export let confirmText = 'Confirm';
@@ -12,7 +11,7 @@
export let cancelColor: Color = 'primary';
export let hideCancelButton = false;
export let disabled = false;
export let width: 'wide' | 'narrow' = 'narrow';
export let width = 500;
export let onClose: () => void;
export let onConfirm: () => void;
@@ -24,21 +23,35 @@
};
</script>
<FullScreenModal {title} {id} {onClose} {width}>
<div class="text-md py-5 text-center">
<slot name="prompt">
<p>{prompt}</p>
</slot>
</div>
<FullScreenModal {onClose}>
<div
class="max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
style="width: {width}px"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="pb-2 text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h1>
</div>
<div>
<div class="text-md px-4 py-5 text-center">
<slot name="prompt">
<p>{prompt}</p>
</slot>
</div>
<div class="mt-4 flex flex-col sm:flex-row w-full gap-4">
{#if !hideCancelButton}
<Button color={cancelColor} fullwidth on:click={onClose}>
{cancelText}
</Button>
{/if}
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
{confirmText}
</Button>
<div class="mt-4 flex w-full gap-4 px-4">
{#if !hideCancelButton}
<Button color={cancelColor} fullwidth on:click={onClose}>
{cancelText}
</Button>
{/if}
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
{confirmText}
</Button>
</div>
</div>
</div>
</FullScreenModal>
@@ -2,44 +2,8 @@
import { clickOutside } from '../../utils/click-outside';
import { fade } from 'svelte/transition';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
export let onClose: () => void;
/**
* Unique identifier for the modal.
*/
export let id: string;
export let title: string;
/**
* If true, the logo will be displayed next to the modal title.
*/
export let showLogo = false;
/**
* Optional icon to display next to the modal title, if `showLogo` is false.
*/
export let icon: string | undefined = undefined;
/**
* Sets the width of the modal.
*
* - `wide`: 750px
* - `narrow`: 450px
* - `auto`: fits the width of the modal content, up to a maximum of 550px
*/
export let width: 'wide' | 'narrow' | 'auto' = 'narrow';
$: titleId = `${id}-title`;
let modalWidth: string;
$: {
if (width === 'wide') {
modalWidth = 'w-[750px]';
} else if (width === 'narrow') {
modalWidth = 'w-[450px]';
} else {
modalWidth = 'sm:max-w-[550px]';
}
}
export let onClose: (() => void) | undefined = undefined;
</script>
<FocusTrap>
@@ -48,17 +12,8 @@
out:fade={{ duration: 100 }}
class="fixed left-0 top-0 z-[9990] flex h-screen w-screen place-content-center place-items-center bg-black/40"
>
<div
class="z-[9999] max-w-[95vw] max-h-[95vh] {modalWidth} overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
tabindex="-1"
aria-modal="true"
aria-labelledby={titleId}
>
<ModalHeader id={titleId} {title} {showLogo} {icon} on:close={() => onClose?.()} />
<div class="p-5 pt-0">
<slot />
</div>
<div class="z-[9999]" use:clickOutside={{ onOutclick: onClose, onEscape: onClose }} tabindex="-1">
<slot />
</div>
</section>
</FocusTrap>
@@ -1,40 +0,0 @@
<script lang="ts">
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { createEventDispatcher } from 'svelte';
import { mdiClose } from '@mdi/js';
const dispatch = createEventDispatcher<{
close: void;
}>();
/**
* Unique identifier for the header text.
*/
export let id: string;
export let title: string;
/**
* If true, the logo will be displayed next to the modal title.
*/
export let showLogo = false;
/**
* Optional icon to display next to the modal title, if `showLogo` is false.
*/
export let icon: string | undefined = undefined;
</script>
<div class="flex place-items-center justify-between px-5 py-3">
<div class="flex gap-2 place-items-center">
{#if showLogo}
<ImmichLogo noText={true} width={32} />
{:else if icon}
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
{/if}
<h1 {id}>
{title}
</h1>
</div>
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} title="Close" />
</div>
@@ -1,5 +1,7 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
import { mdiClose } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import FullScreenModal from '../full-screen-modal.svelte';
import UserAvatar from '../user-avatar.svelte';
@@ -13,14 +15,28 @@
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
</script>
<FullScreenModal id="avatar-selector-modal" title="Select avatar color" width="auto" onClose={() => dispatch('close')}>
<div class="flex items-center justify-center mt-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
{#each colors as color}
<button on:click={() => dispatch('choose', color)}>
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
</button>
{/each}
<FullScreenModal onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div
class=" rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg p-4"
>
<div class="flex items-center">
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary text-sm">
SELECT AVATAR COLOR
</h1>
<div>
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
</div>
</div>
<div class="flex items-center justify-center p-4 mt-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
{#each colors as color}
<button on:click={() => dispatch('choose', color)}>
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
</button>
{/each}
</div>
</div>
</div>
</div>
</FullScreenModal>
@@ -1,7 +1,8 @@
<script lang="ts">
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { createEventDispatcher } from 'svelte';
import FullScreenModal from './full-screen-modal.svelte';
import { mdiInformationOutline } from '@mdi/js';
import { mdiClose, mdiInformationOutline } from '@mdi/js';
import Icon from '../elements/icon.svelte';
interface Shortcuts {
@@ -37,51 +38,63 @@
}>();
</script>
<FullScreenModal
id="keyboard-shortcuts-modal"
title="Keyboard shortcuts"
width="auto"
onClose={() => dispatch('close')}
>
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
<div class="p-4">
<h2>General</h2>
<div class="text-sm">
{#each shortcuts.general as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
{key}
</p>
{/each}
</div>
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
</div>
{/each}
<FullScreenModal onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div
class="rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div class="relative px-4 pt-4">
<h1 class="px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">Keyboard Shortcuts</h1>
<div class="absolute inset-y-0 right-0 px-4 py-4">
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
</div>
</div>
</div>
<div class="p-4">
<h2>Actions</h2>
<div class="text-sm">
{#each shortcuts.actions as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
{key}
</p>
{/each}
</div>
<div class="flex items-center gap-2">
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
{#if shortcut.info}
<Icon path={mdiInformationOutline} title={shortcut.info} />
{/if}
</div>
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
<div class="px-4 py-4">
<h2>General</h2>
<div class="text-sm">
{#each shortcuts.general as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p
class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2"
>
{key}
</p>
{/each}
</div>
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
</div>
{/each}
</div>
{/each}
</div>
<div class="px-4 py-4">
<h2>Actions</h2>
<div class="text-sm">
{#each shortcuts.actions as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p
class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2"
>
{key}
</p>
{/each}
</div>
<div class="flex items-center gap-2">
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
{#if shortcut.info}
<Icon path={mdiInformationOutline} title={shortcut.info} />
{/if}
</div>
</div>
{/each}
</div>
</div>
</div>
</div>
</div>
@@ -33,28 +33,34 @@
</script>
{#if showModal}
<FullScreenModal id="new-version-modal" title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}>
<div>
Hi friend, there is a new version of the application please take your time to visit the
<span class="font-medium underline"
><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
>release notes</a
></span
>
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
especially if you use WatchTower or any mechanism that handles updating your application automatically.
</div>
<FullScreenModal onClose={() => (showModal = false)}>
<div
class="max-w-lg rounded-3xl border bg-immich-bg px-8 py-10 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<p class="mb-4 text-2xl">🎉 NEW VERSION AVAILABLE 🎉</p>
<div class="mt-4 font-medium">Your friend, Alex</div>
<div>
Hi friend, there is a new version of the application please take your time to visit the
<span class="font-medium underline"
><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
>release notes</a
></span
>
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
especially if you use WatchTower or any mechanism that handles updating your application automatically.
</div>
<div class="font-sm mt-8">
<code>Server Version: {serverVersion}</code>
<br />
<code>Latest Version: {releaseVersion}</code>
</div>
<div class="mt-4 font-medium">Your friend, Alex</div>
<div class="mt-8 text-right">
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
<div class="font-sm mt-8">
<code>Server Version: {serverVersion}</code>
<br />
<code>Latest Version: {releaseVersion}</code>
</div>
<div class="mt-8 text-right">
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
</div>
</div>
</FullScreenModal>
{/if}
@@ -30,22 +30,31 @@
};
</script>
<FullScreenModal id="slideshow-settings-modal" title="Slideshow settings" {onClose}>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title="Direction"
options={Object.values(options)}
selectedOption={options[$slideshowNavigation]}
onToggle={(option) => handleToggle(option)}
/>
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label="Duration"
desc="Number of seconds to display each image"
min={1}
bind:value={$slideshowDelay}
/>
<Button class="w-full" color="gray" on:click={onClose}>Done</Button>
<FullScreenModal {onClose}>
<div
class="flex w-full md:w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
>
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
Slideshow Settings
</h1>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title="Direction"
options={Object.values(options)}
selectedOption={options[$slideshowNavigation]}
onToggle={(option) => handleToggle(option)}
/>
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label="Duration"
desc="Number of seconds to display each image"
min={1}
bind:value={$slideshowDelay}
/>
<Button class="w-full" color="gray" on:click={onClose}>Done</Button>
</div>
</div>
</FullScreenModal>
@@ -49,7 +49,6 @@
{#if deleteDevice}
<ConfirmDialogue
id="log-out-device-modal"
prompt="Are you sure you want to log out this device?"
onConfirm={() => handleDelete()}
onClose={() => (deleteDevice = null)}
@@ -58,7 +57,6 @@
{#if deleteAll}
<ConfirmDialogue
id="log-out-all-modal"
prompt="Are you sure you want to log out all devices?"
onConfirm={() => handleDeleteAll()}
onClose={() => (deleteAll = false)}
@@ -189,7 +189,6 @@
{#if removePartnerDto}
<ConfirmDialogue
id="stop-sharing-photos-modal"
title="Stop sharing your photos?"
prompt="{removePartnerDto.name} will no longer be able to access your photos."
onClose={() => (removePartnerDto = null)}
@@ -81,7 +81,7 @@
{#if newKey}
<APIKeyForm
title="New API key"
title="New API Key"
submitText="Create"
apiKey={newKey}
on:submit={({ detail }) => handleCreate(detail)}
@@ -95,7 +95,6 @@
{#if editKey}
<APIKeyForm
title="API key"
submitText="Save"
apiKey={editKey}
on:submit={({ detail }) => handleUpdate(detail)}
@@ -105,8 +104,7 @@
{#if deleteKey}
<ConfirmDialogue
id="confirm-api-key-delete-modal"
prompt="Are you sure you want to delete this API key?"
prompt="Are you sure you want to delete this API Key?"
onConfirm={() => handleDelete()}
onClose={() => (deleteKey = null)}
/>
@@ -683,7 +683,6 @@
{#if viewMode === ViewMode.CONFIRM_DELETE}
<ConfirmDialogue
id="delete-album-modal"
title="Delete album"
confirmText="Delete"
onConfirm={handleRemoveAlbum}
+27 -17
View File
@@ -463,25 +463,35 @@
{/if}
{#if showChangeNameModal}
<FullScreenModal id="change-name-modal" title="Change name" onClose={() => (showChangeNameModal = false)}>
<form on:submit|preventDefault={submitNameChange} autocomplete="off">
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<!-- svelte-ignore a11y-autofocus -->
<input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus />
<FullScreenModal onClose={() => (showChangeNameModal = false)}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Change name</h1>
</div>
<div class="mt-8 flex w-full gap-4">
<Button
color="gray"
fullwidth
on:click={() => {
showChangeNameModal = false;
}}>Cancel</Button
>
<Button type="submit" fullwidth>Ok</Button>
</div>
</form>
<form on:submit|preventDefault={submitNameChange} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<!-- svelte-ignore a11y-autofocus -->
<input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus />
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button
color="gray"
fullwidth
on:click={() => {
showChangeNameModal = false;
}}>Cancel</Button
>
<Button type="submit" fullwidth>Ok</Button>
</div>
</form>
</div>
</FullScreenModal>
{/if}
@@ -88,8 +88,7 @@
{#if deleteLinkId}
<ConfirmDialogue
id="delete-shared-link-modal"
title="Delete shared link"
title="Delete Shared Link"
prompt="Are you sure you want to delete this shared link?"
confirmText="Delete"
onConfirm={() => handleDeleteLink()}
+1 -2
View File
@@ -98,8 +98,7 @@
{#if isShowEmptyConfirmation}
<ConfirmDialogue
id="empty-trash-modal"
title="Empty trash"
title="Empty Trash"
confirmText="Empty"
onConfirm={handleEmptyTrash}
onClose={() => (isShowEmptyConfirmation = false)}
@@ -118,9 +118,7 @@
const handleCreate = async (ownerId: string) => {
try {
const createdLibrary = await createLibrary({
createLibraryDto: { ownerId, type: LibraryType.External },
});
const createdLibrary = await createLibrary({ createLibraryDto: { ownerId, type: LibraryType.External } });
notificationController.show({
message: `Created library: ${createdLibrary.name}`,
@@ -302,7 +300,6 @@
{#if confirmDeleteLibrary}
<ConfirmDialogue
id="warning-modal"
title="Warning!"
prompt="Are you sure you want to delete this library? This will delete all {deleteAssetCount} contained assets from Immich and cannot be undone. Files will remain on disk."
onConfirm={handleDelete}
@@ -1,9 +1,9 @@
<script lang="ts">
import { page } from '$app/stores';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
@@ -21,14 +21,7 @@
import { asByteUnitString } from '$lib/utils/byte-units';
import { copyToClipboard } from '$lib/utils';
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
import {
mdiAccountEditOutline,
mdiClose,
mdiContentCopy,
mdiDeleteRestore,
mdiPencilOutline,
mdiTrashCanOutline,
} from '@mdi/js';
import { mdiClose, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
import { DateTime } from 'luxon';
import { onMount } from 'svelte';
import type { PageData } from './$types';
@@ -123,23 +116,13 @@
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 lg:w-[850px]">
{#if shouldShowCreateUserForm}
<FullScreenModal
id="create-new-user-modal"
title="Create new user"
showLogo
onClose={() => (shouldShowCreateUserForm = false)}
>
<FullScreenModal onClose={() => (shouldShowCreateUserForm = false)}>
<CreateUserForm on:submit={onUserCreated} on:cancel={() => (shouldShowCreateUserForm = false)} />
</FullScreenModal>
{/if}
{#if shouldShowEditUserForm}
<FullScreenModal
id="edit-user-modal"
title="Edit user"
icon={mdiAccountEditOutline}
onClose={() => (shouldShowEditUserForm = false)}
>
<FullScreenModal onClose={() => (shouldShowEditUserForm = false)}>
<EditUserForm
user={selectedUser}
bind:newPassword
@@ -170,39 +153,40 @@
{/if}
{#if shouldShowPasswordResetSuccess}
<ConfirmDialogue
id="password-reset-success-modal"
title="Password reset success"
confirmText="Done"
onConfirm={() => (shouldShowPasswordResetSuccess = false)}
onClose={() => (shouldShowPasswordResetSuccess = false)}
hideCancelButton={true}
confirmColor="green"
>
<svelte:fragment slot="prompt">
<div class="flex flex-col gap-4">
<p>The user's password has been reset:</p>
<FullScreenModal onClose={() => (shouldShowPasswordResetSuccess = false)}>
<ConfirmDialogue
title="Password Reset Success"
confirmText="Done"
onConfirm={() => (shouldShowPasswordResetSuccess = false)}
onClose={() => (shouldShowPasswordResetSuccess = false)}
hideCancelButton={true}
confirmColor="green"
>
<svelte:fragment slot="prompt">
<div class="flex flex-col gap-4">
<p>The user's password has been reset:</p>
<div class="flex justify-center gap-2">
<code
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
>
{newPassword}
</code>
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiContentCopy} size="18" />
</div>
</LinkButton>
<div class="flex justify-center gap-2">
<code
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
>
{newPassword}
</code>
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiContentCopy} size="18" />
</div>
</LinkButton>
</div>
<p>
Please provide the temporary password to the user and inform them they will need to change the
password at their next login.
</p>
</div>
<p>
Please provide the temporary password to the user and inform them they will need to change the password
at their next login.
</p>
</div>
</svelte:fragment>
</ConfirmDialogue>
</svelte:fragment>
</ConfirmDialogue>
</FullScreenModal>
{/if}
<table class="my-5 w-full text-left">