You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-03 05:46:58 +02:00
Merge remote-tracking branch 'origin/main' into fix-search-scrollbar
This commit is contained in:
4
.github/workflows/prepare-release.yml
vendored
4
.github/workflows/prepare-release.yml
vendored
@ -41,8 +41,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Install Poetry
|
- name: Install uv
|
||||||
run: pipx install poetry
|
uses: astral-sh/setup-uv@v5
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
|
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
|
||||||
|
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
@ -380,27 +380,28 @@ jobs:
|
|||||||
working-directory: ./machine-learning
|
working-directory: ./machine-learning
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install poetry
|
- name: Install uv
|
||||||
run: pipx install poetry
|
uses: astral-sh/setup-uv@v5
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
|
||||||
python-version: 3.11
|
# with:
|
||||||
cache: 'poetry'
|
# python-version: 3.11
|
||||||
|
# cache: 'uv'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
poetry install --with dev --with cpu
|
uv sync --extra cpu
|
||||||
- name: Lint with ruff
|
- name: Lint with ruff
|
||||||
run: |
|
run: |
|
||||||
poetry run ruff check --output-format=github app export
|
uv run ruff check --output-format=github app export
|
||||||
- name: Check black formatting
|
- name: Check black formatting
|
||||||
run: |
|
run: |
|
||||||
poetry run black --check app export
|
uv run black --check app export
|
||||||
- name: Run mypy type checking
|
- name: Run mypy type checking
|
||||||
run: |
|
run: |
|
||||||
poetry run mypy --install-types --non-interactive --strict app/
|
uv run mypy --strict app/
|
||||||
- name: Run tests and coverage
|
- name: Run tests and coverage
|
||||||
run: |
|
run: |
|
||||||
poetry run pytest app --cov=app --cov-report term-missing
|
uv run pytest app --cov=app --cov-report term-missing
|
||||||
|
|
||||||
shellcheck:
|
shellcheck:
|
||||||
name: ShellCheck
|
name: ShellCheck
|
||||||
|
@ -25,7 +25,7 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
target: dev
|
target: dev
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ../server:/usr/src/app
|
||||||
- ../open-api:/usr/src/open-api
|
- ../open-api:/usr/src/open-api
|
||||||
|
@ -53,7 +53,7 @@ docker compose create # Create Docker containers for Immich apps witho
|
|||||||
docker start immich_postgres # Start Postgres server
|
docker start immich_postgres # Start Postgres server
|
||||||
sleep 10 # Wait for Postgres server to start up
|
sleep 10 # Wait for Postgres server to start up
|
||||||
# Check the database user if you deviated from the default
|
# Check the database user if you deviated from the default
|
||||||
gunzip < "/path/to/backup/dump.sql.gz" \
|
gunzip --stdout "/path/to/backup/dump.sql.gz" \
|
||||||
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
|
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
|
||||||
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
|
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
|
||||||
docker compose up -d # Start remainder of Immich apps
|
docker compose up -d # Start remainder of Immich apps
|
||||||
@ -76,8 +76,8 @@ docker compose create # Create Docker containers for
|
|||||||
docker start immich_postgres # Start Postgres server
|
docker start immich_postgres # Start Postgres server
|
||||||
sleep 10 # Wait for Postgres server to start up
|
sleep 10 # Wait for Postgres server to start up
|
||||||
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
||||||
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip`
|
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip --stdout`
|
||||||
cat < "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
|
cat "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
|
||||||
exit # Exit the Docker shell
|
exit # Exit the Docker shell
|
||||||
docker compose up -d # Start remainder of Immich apps
|
docker compose up -d # Start remainder of Immich apps
|
||||||
```
|
```
|
||||||
|
@ -201,7 +201,7 @@ describe('/people', () => {
|
|||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: 'New Person',
|
name: 'New Person',
|
||||||
birthDate: '1990-01-01T00:00:00.000Z',
|
birthDate: '1990-01-01',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -262,7 +262,7 @@ describe('/people', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ birthDate: '1990-01-01' });
|
.send({ birthDate: '1990-01-01' });
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({ birthDate: '1990-01-01T00:00:00.000Z' });
|
expect(body).toMatchObject({ birthDate: '1990-01-01' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear a date of birth', async () => {
|
it('should clear a date of birth', async () => {
|
||||||
|
@ -19,20 +19,16 @@ FROM builder-${DEVICE} AS builder
|
|||||||
|
|
||||||
ARG DEVICE
|
ARG DEVICE
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1
|
||||||
PIP_NO_CACHE_DIR=true \
|
WORKDIR /usr/src/app
|
||||||
VIRTUAL_ENV="/opt/venv" \
|
|
||||||
PATH="/opt/venv/bin:${PATH}"
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||||
|
|
||||||
RUN pip install --upgrade pip && pip install poetry
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
RUN poetry config installer.max-workers 10 && \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
poetry config virtualenvs.create false
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
RUN python3 -m venv /opt/venv
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress
|
||||||
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:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu
|
FROM python:3.11-slim-bookworm@sha256:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu
|
||||||
|
|
||||||
@ -93,7 +89,7 @@ WORKDIR /usr/src/app
|
|||||||
ENV TRANSFORMERS_CACHE=/cache \
|
ENV TRANSFORMERS_CACHE=/cache \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH="/opt/venv/bin:$PATH" \
|
PATH="/usr/src/app/.venv/bin:$PATH" \
|
||||||
PYTHONPATH=/usr/src \
|
PYTHONPATH=/usr/src \
|
||||||
DEVICE=${DEVICE}
|
DEVICE=${DEVICE}
|
||||||
|
|
||||||
@ -102,7 +98,7 @@ RUN echo "hard core 0" >> /etc/security/limits.conf && \
|
|||||||
echo "fs.suid_dumpable 0" >> /etc/sysctl.conf && \
|
echo "fs.suid_dumpable 0" >> /etc/sysctl.conf && \
|
||||||
echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile
|
echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile
|
||||||
|
|
||||||
COPY --from=builder /opt/venv /opt/venv
|
COPY --from=builder /usr/src/app/.venv /usr/src/app/.venv
|
||||||
COPY ann/ann.py /usr/src/ann/ann.py
|
COPY ann/ann.py /usr/src/ann/ann.py
|
||||||
COPY start.sh log_conf.json gunicorn_conf.py ./
|
COPY start.sh log_conf.json gunicorn_conf.py ./
|
||||||
COPY app .
|
COPY app .
|
||||||
|
@ -5,13 +5,12 @@
|
|||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
This project uses [Poetry](https://python-poetry.org/docs/#installation), so be sure to install it first.
|
This project uses [uv](https://docs.astral.sh/uv/getting-started/installation/), so be sure to install it first.
|
||||||
Running `poetry install --no-root --with dev --with cpu` will install everything you need in an isolated virtual environment.
|
Running `uv sync --extra cpu` will install everything you need in an isolated virtual environment.
|
||||||
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
|
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--group cpu` with either of `--group cuda` or `--group openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
|
||||||
|
|
||||||
To add or remove dependencies, you can use the commands `poetry add $PACKAGE_NAME` and `poetry remove $PACKAGE_NAME`, respectively.
|
|
||||||
Be sure to commit the `poetry.lock` and `pyproject.toml` files with `poetry lock --no-update` to reflect any changes in dependencies.
|
|
||||||
|
|
||||||
|
To add or remove dependencies, you can use the commands `uv add $PACKAGE_NAME` and `uv remove $PACKAGE_NAME`, respectively.
|
||||||
|
Be sure to commit the `uv.lock` and `pyproject.toml` files with `uv lock` to reflect any changes in dependencies.
|
||||||
|
|
||||||
# Load Testing
|
# Load Testing
|
||||||
|
|
||||||
@ -26,15 +25,18 @@ Note that in Locust's jargon, concurrency is measured in `users`, and each user
|
|||||||
# Facial Recognition
|
# Facial Recognition
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
This project utilizes facial recognition models from the [InsightFace](https://github.com/deepinsight/insightface/tree/master/model_zoo) project. We appreciate the work put into developing these models, which have been beneficial to the machine learning part of this project.
|
This project utilizes facial recognition models from the [InsightFace](https://github.com/deepinsight/insightface/tree/master/model_zoo) project. We appreciate the work put into developing these models, which have been beneficial to the machine learning part of this project.
|
||||||
|
|
||||||
### Used Models
|
### Used Models
|
||||||
* antelopev2
|
|
||||||
* buffalo_l
|
- antelopev2
|
||||||
* buffalo_m
|
- buffalo_l
|
||||||
* buffalo_s
|
- buffalo_m
|
||||||
|
- buffalo_s
|
||||||
|
|
||||||
## License and Use Restrictions
|
## License and Use Restrictions
|
||||||
|
|
||||||
We have received permission to use the InsightFace facial recognition models in our project, as granted via email by Jia Guo (guojia@insightface.ai) on 18th March 2023. However, it's important to note that this permission does not extend to the redistribution or commercial use of their models by third parties. Users and developers interested in using these models should review the licensing terms provided in the InsightFace GitHub repository.
|
We have received permission to use the InsightFace facial recognition models in our project, as granted via email by Jia Guo (guojia@insightface.ai) on 18th March 2023. However, it's important to note that this permission does not extend to the redistribution or commercial use of their models by third parties. Users and developers interested in using these models should review the licensing terms provided in the InsightFace GitHub repository.
|
||||||
|
|
||||||
For more information on the capabilities of the InsightFace models and to ensure compliance with their license, please refer to their [official repository](https://github.com/deepinsight/insightface). Adhering to the specified licensing terms is crucial for the respectful and lawful use of their work.
|
For more information on the capabilities of the InsightFace models and to ensure compliance with their license, please refer to their [official repository](https://github.com/deepinsight/insightface). Adhering to the specified licensing terms is crucial for the respectful and lawful use of their work.
|
3738
machine-learning/poetry.lock
generated
3738
machine-learning/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,72 +1,77 @@
|
|||||||
[tool.poetry]
|
[project]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.129.0"
|
version = "1.129.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||||
|
requires-python = ">=3.10,<4.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [{include = "app"}]
|
dependencies = [
|
||||||
|
"aiocache>=0.12.1,<1.0",
|
||||||
|
"fastapi>=0.95.2,<1.0",
|
||||||
|
"ftfy>=6.1.1",
|
||||||
|
"gunicorn>=21.1.0",
|
||||||
|
"huggingface-hub>=0.20.1,<1.0",
|
||||||
|
"insightface>=0.7.3,<1.0",
|
||||||
|
"opencv-python-headless>=4.7.0.72,<5.0",
|
||||||
|
"orjson>=3.9.5",
|
||||||
|
"pillow>=9.5.0,<11.0",
|
||||||
|
"pydantic>=2.0.0,<3",
|
||||||
|
"pydantic-settings>=2.5.2,<3",
|
||||||
|
"python-multipart>=0.0.6,<1.0",
|
||||||
|
"rich>=13.4.2",
|
||||||
|
"tokenizers>=0.15.0,<1.0",
|
||||||
|
"uvicorn[standard]>=0.22.0,<1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[dependency-groups]
|
||||||
python = ">=3.10,<4.0"
|
test = [
|
||||||
insightface = ">=0.7.3,<1.0"
|
"httpx>=0.24.1",
|
||||||
opencv-python-headless = ">=4.7.0.72,<5.0"
|
"pytest>=7.3.1",
|
||||||
pillow = ">=9.5.0,<11.0"
|
"pytest-asyncio>=0.21.0",
|
||||||
fastapi = ">=0.95.2,<1.0"
|
"pytest-cov>=4.1.0",
|
||||||
uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"}
|
"pytest-mock>=3.11.1",
|
||||||
pydantic = "^2.0.0"
|
]
|
||||||
pydantic-settings = "^2.5.2"
|
types = [
|
||||||
aiocache = ">=0.12.1,<1.0"
|
"types-pyyaml>=6.0.12.20241230",
|
||||||
rich = ">=13.4.2"
|
"types-requests>=2.32.0.20250306",
|
||||||
ftfy = ">=6.1.1"
|
"types-setuptools>=75.8.2.20250305",
|
||||||
python-multipart = ">=0.0.6,<1.0"
|
"types-simplejson>=3.20.0.20250218",
|
||||||
orjson = ">=3.9.5"
|
"types-ujson>=5.10.0.20240515",
|
||||||
gunicorn = ">=21.1.0"
|
]
|
||||||
huggingface-hub = ">=0.20.1,<1.0"
|
lint = [
|
||||||
tokenizers = ">=0.15.0,<1.0"
|
"black>=23.3.0",
|
||||||
|
"mypy>=1.3.0",
|
||||||
|
"ruff>=0.0.272",
|
||||||
|
{ include-group = "types" },
|
||||||
|
]
|
||||||
|
dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }]
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[project.optional-dependencies]
|
||||||
mypy = ">=1.3.0"
|
cpu = ["onnxruntime>=1.15.0,<2"]
|
||||||
black = ">=23.3.0"
|
cuda = ["onnxruntime-gpu>=1.17.0,<2"]
|
||||||
pytest = ">=7.3.1"
|
openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0"]
|
||||||
locust = ">=2.15.1"
|
armnn = ["onnxruntime>=1.15.0,<2"]
|
||||||
httpx = ">=0.24.1"
|
|
||||||
pytest-asyncio = ">=0.21.0"
|
|
||||||
pytest-cov = ">=4.1.0"
|
|
||||||
ruff = ">=0.0.272"
|
|
||||||
pytest-mock = ">=3.11.1"
|
|
||||||
|
|
||||||
[tool.poetry.group.cpu]
|
[tool.uv]
|
||||||
optional = true
|
compile-bytecode = true
|
||||||
|
|
||||||
[tool.poetry.group.cpu.dependencies]
|
[[tool.uv.index]]
|
||||||
onnxruntime = "^1.15.0"
|
|
||||||
|
|
||||||
[tool.poetry.group.cuda]
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[tool.poetry.group.cuda.dependencies]
|
|
||||||
onnxruntime-gpu = {version = "^1.17.0", source = "cuda12"}
|
|
||||||
|
|
||||||
[tool.poetry.group.openvino]
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[tool.poetry.group.openvino.dependencies]
|
|
||||||
onnxruntime-openvino = ">=1.17.1,<1.19.0"
|
|
||||||
|
|
||||||
[tool.poetry.group.armnn]
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[tool.poetry.group.armnn.dependencies]
|
|
||||||
onnxruntime = "^1.15.0"
|
|
||||||
|
|
||||||
[[tool.poetry.source]]
|
|
||||||
name = "cuda12"
|
name = "cuda12"
|
||||||
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
|
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
|
||||||
priority = "explicit"
|
explicit = true
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
onnxruntime-gpu = { index = "cuda12" }
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = ["app"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
include = ["app"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["hatchling"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.11"
|
python_version = "3.11"
|
||||||
|
2648
machine-learning/uv.lock
generated
Normal file
2648
machine-learning/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -73,7 +73,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
|||||||
npm --prefix web i --package-lock-only
|
npm --prefix web i --package-lock-only
|
||||||
npm --prefix e2e version "$SERVER_PUMP"
|
npm --prefix e2e version "$SERVER_PUMP"
|
||||||
npm --prefix e2e i --package-lock-only
|
npm --prefix e2e i --package-lock-only
|
||||||
poetry --directory machine-learning version "$SERVER_PUMP"
|
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "$SERVER_PUMP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
|
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
|
||||||
|
@ -93,9 +93,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.linusu.flutter_web_auth.CallbackActivity"
|
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter android:label="flutter_web_auth">
|
<intent-filter android:label="flutter_web_auth_2">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
@ -264,6 +264,7 @@
|
|||||||
"exif_bottom_sheet_location_add": "Add a location",
|
"exif_bottom_sheet_location_add": "Add a location",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
"exif_bottom_sheet_people": "PEOPLE",
|
||||||
"exif_bottom_sheet_person_add_person": "Add name",
|
"exif_bottom_sheet_person_add_person": "Add name",
|
||||||
|
"exif_bottom_sheet_person_age": "Age {}",
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
|
@ -48,7 +48,7 @@ PODS:
|
|||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_web_auth (0.6.0):
|
- flutter_web_auth_2 (3.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -117,7 +117,7 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`)
|
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
@ -166,8 +166,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
flutter_web_auth:
|
flutter_web_auth_2:
|
||||||
:path: ".symlinks/plugins/flutter_web_auth/ios"
|
:path: ".symlinks/plugins/flutter_web_auth_2/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
geolocator_apple:
|
geolocator_apple:
|
||||||
@ -220,7 +220,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_web_auth: acc15a8fd7bba796a933c724a6dffc3d00f07c27
|
flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
|
||||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||||
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
|
@ -8,20 +8,26 @@ class SearchCuratedContent {
|
|||||||
/// The label to show associated with this curated object
|
/// The label to show associated with this curated object
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
/// The subtitle to show below the label
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
/// The id to lookup the asset from the server
|
/// The id to lookup the asset from the server
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
SearchCuratedContent({
|
SearchCuratedContent({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
this.subtitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
SearchCuratedContent copyWith({
|
SearchCuratedContent copyWith({
|
||||||
String? label,
|
String? label,
|
||||||
|
String? subtitle,
|
||||||
String? id,
|
String? id,
|
||||||
}) {
|
}) {
|
||||||
return SearchCuratedContent(
|
return SearchCuratedContent(
|
||||||
label: label ?? this.label,
|
label: label ?? this.label,
|
||||||
|
subtitle: subtitle ?? this.subtitle,
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -29,6 +35,7 @@ class SearchCuratedContent {
|
|||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'label': label,
|
'label': label,
|
||||||
|
'subtitle': subtitle,
|
||||||
'id': id,
|
'id': id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -36,6 +43,7 @@ class SearchCuratedContent {
|
|||||||
factory SearchCuratedContent.fromMap(Map<String, dynamic> map) {
|
factory SearchCuratedContent.fromMap(Map<String, dynamic> map) {
|
||||||
return SearchCuratedContent(
|
return SearchCuratedContent(
|
||||||
label: map['label'] as String,
|
label: map['label'] as String,
|
||||||
|
subtitle: map['subtitle'] as String?,
|
||||||
id: map['id'] as String,
|
id: map['id'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -46,13 +54,14 @@ class SearchCuratedContent {
|
|||||||
SearchCuratedContent.fromMap(json.decode(source) as Map<String, dynamic>);
|
SearchCuratedContent.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'CuratedContent(label: $label, id: $id)';
|
String toString() =>
|
||||||
|
'CuratedContent(label: $label, subtitle: $subtitle, id: $id)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant SearchCuratedContent other) {
|
bool operator ==(covariant SearchCuratedContent other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other.label == label && other.id == id;
|
return other.label == label && other.subtitle == subtitle && other.id == id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
4
mobile/lib/providers/activity.provider.g.dart
generated
4
mobile/lib/providers/activity.provider.g.dart
generated
@ -187,6 +187,8 @@ class AlbumActivityProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef<List<Activity>> {
|
mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef<List<Activity>> {
|
||||||
/// The parameter `albumId` of this provider.
|
/// The parameter `albumId` of this provider.
|
||||||
String get albumId;
|
String get albumId;
|
||||||
@ -206,4 +208,4 @@ class _AlbumActivityProviderElement
|
|||||||
String? get assetId => (origin as AlbumActivityProvider).assetId;
|
String? get assetId => (origin as AlbumActivityProvider).assetId;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/repositories/activity_api.repository.dart';
|
import 'package:immich_mobile/repositories/activity_api.repository.dart';
|
||||||
import 'package:immich_mobile/services/activity.service.dart';
|
import 'package:immich_mobile/services/activity.service.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@ -5,5 +6,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'activity_service.provider.g.dart';
|
part 'activity_service.provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
ActivityService activityService(ActivityServiceRef ref) =>
|
ActivityService activityService(Ref ref) =>
|
||||||
ActivityService(ref.watch(activityApiRepositoryProvider));
|
ActivityService(ref.watch(activityApiRepositoryProvider));
|
||||||
|
@ -20,6 +20,8 @@ final activityServiceProvider = AutoDisposeProvider<ActivityService>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>;
|
typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -186,6 +186,8 @@ class ActivityStatisticsProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef<int> {
|
mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef<int> {
|
||||||
/// The parameter `albumId` of this provider.
|
/// The parameter `albumId` of this provider.
|
||||||
String get albumId;
|
String get albumId;
|
||||||
@ -205,4 +207,4 @@ class _ActivityStatisticsProviderElement
|
|||||||
String? get assetId => (origin as ActivityStatisticsProvider).assetId;
|
String? get assetId => (origin as ActivityStatisticsProvider).assetId;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -40,4 +40,4 @@ final albumSortOrderProvider =
|
|||||||
|
|
||||||
typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>;
|
typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -22,4 +22,4 @@ final currentAlbumProvider =
|
|||||||
|
|
||||||
typedef _$CurrentAlbum = AutoDisposeNotifier<Album?>;
|
typedef _$CurrentAlbum = AutoDisposeNotifier<Album?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'api.provider.g.dart';
|
part 'api.provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
ApiService apiService(ApiServiceRef ref) => ApiService();
|
ApiService apiService(Ref ref) => ApiService();
|
||||||
|
4
mobile/lib/providers/api.provider.g.dart
generated
4
mobile/lib/providers/api.provider.g.dart
generated
@ -19,6 +19,8 @@ final apiServiceProvider = Provider<ApiService>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef ApiServiceRef = ProviderRef<ApiService>;
|
typedef ApiServiceRef = ProviderRef<ApiService>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'app_settings.provider.g.dart';
|
part 'app_settings.provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
AppSettingsService appSettingsService(AppSettingsServiceRef ref) =>
|
AppSettingsService appSettingsService(Ref ref) => AppSettingsService();
|
||||||
AppSettingsService();
|
|
||||||
|
@ -21,6 +21,8 @@ final appSettingsServiceProvider = Provider<AppSettingsService>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef AppSettingsServiceRef = ProviderRef<AppSettingsService>;
|
typedef AppSettingsServiceRef = ProviderRef<AppSettingsService>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -31,7 +31,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
final SyncService _syncService;
|
final SyncService _syncService;
|
||||||
final ETagService _etagService;
|
final ETagService _etagService;
|
||||||
final ExifService _exifService;
|
final ExifService _exifService;
|
||||||
final StateNotifierProviderRef _ref;
|
final Ref _ref;
|
||||||
final log = Logger('AssetNotifier');
|
final log = Logger('AssetNotifier');
|
||||||
bool _getAllAssetInProgress = false;
|
bool _getAllAssetInProgress = false;
|
||||||
bool _deleteInProgress = false;
|
bool _deleteInProgress = false;
|
||||||
|
@ -171,6 +171,8 @@ class AssetPeopleNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin AssetPeopleNotifierRef
|
mixin AssetPeopleNotifierRef
|
||||||
on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> {
|
on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> {
|
||||||
/// The parameter `asset` of this provider.
|
/// The parameter `asset` of this provider.
|
||||||
@ -186,4 +188,4 @@ class _AssetPeopleNotifierProviderElement
|
|||||||
Asset get asset => (origin as AssetPeopleNotifierProvider).asset;
|
Asset get asset => (origin as AssetPeopleNotifierProvider).asset;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -39,6 +39,6 @@ final assetStackStateProvider = StateNotifierProvider.autoDispose
|
|||||||
);
|
);
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
int assetStackIndex(AssetStackIndexRef ref, Asset asset) {
|
int assetStackIndex(Ref ref, Asset asset) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,8 @@ class AssetStackIndexProvider extends AutoDisposeProvider<int> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin AssetStackIndexRef on AutoDisposeProviderRef<int> {
|
mixin AssetStackIndexRef on AutoDisposeProviderRef<int> {
|
||||||
/// The parameter `asset` of this provider.
|
/// The parameter `asset` of this provider.
|
||||||
Asset get asset;
|
Asset get asset;
|
||||||
@ -155,4 +157,4 @@ class _AssetStackIndexProviderElement extends AutoDisposeProviderElement<int>
|
|||||||
Asset get asset => (origin as AssetStackIndexProvider).asset;
|
Asset get asset => (origin as AssetStackIndexProvider).asset;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -22,4 +22,4 @@ final currentAssetProvider =
|
|||||||
|
|
||||||
typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>;
|
typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -24,4 +24,4 @@ final backupVerificationProvider =
|
|||||||
|
|
||||||
typedef _$BackupVerification = AutoDisposeNotifier<bool>;
|
typedef _$BackupVerification = AutoDisposeNotifier<bool>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'immich_logo_provider.g.dart';
|
part 'immich_logo_provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<Uint8List> immichLogo(ImmichLogoRef ref) async {
|
Future<Uint8List> immichLogo(Ref ref) async {
|
||||||
final json = await rootBundle.loadString('assets/immich-logo.json');
|
final json = await rootBundle.loadString('assets/immich-logo.json');
|
||||||
final j = jsonDecode(json);
|
final j = jsonDecode(json);
|
||||||
return base64Decode(j['content']);
|
return base64Decode(j['content']);
|
||||||
|
4
mobile/lib/providers/immich_logo_provider.g.dart
generated
4
mobile/lib/providers/immich_logo_provider.g.dart
generated
@ -19,6 +19,8 @@ final immichLogoProvider = AutoDisposeFutureProvider<Uint8List>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef ImmichLogoRef = AutoDisposeFutureProviderRef<Uint8List>;
|
typedef ImmichLogoRef = AutoDisposeFutureProviderRef<Uint8List>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'db.provider.g.dart';
|
part 'db.provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Isar isar(IsarRef ref) => throw UnimplementedError('isar');
|
Isar isar(Ref ref) => throw UnimplementedError('isar');
|
||||||
|
@ -19,6 +19,8 @@ final isarProvider = Provider<Isar>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef IsarRef = ProviderRef<Isar>;
|
typedef IsarRef = ProviderRef<Isar>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
@ -6,5 +7,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'exif.provider.g.dart';
|
part 'exif.provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
IExifInfoRepository exifRepository(ExifRepositoryRef ref) =>
|
IExifInfoRepository exifRepository(Ref ref) =>
|
||||||
IsarExifRepository(ref.watch(isarProvider));
|
IsarExifRepository(ref.watch(isarProvider));
|
||||||
|
@ -20,6 +20,8 @@ final exifRepositoryProvider = Provider<IExifInfoRepository>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef ExifRepositoryRef = ProviderRef<IExifInfoRepository>;
|
typedef ExifRepositoryRef = ProviderRef<IExifInfoRepository>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
@ -6,5 +7,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'store.provider.g.dart';
|
part 'store.provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
IStoreRepository storeRepository(StoreRepositoryRef ref) =>
|
IStoreRepository storeRepository(Ref ref) =>
|
||||||
IsarStoreRepository(ref.watch(isarProvider));
|
IsarStoreRepository(ref.watch(isarProvider));
|
||||||
|
@ -20,6 +20,8 @@ final storeRepositoryProvider = AutoDisposeProvider<IStoreRepository>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>;
|
typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_service.provider.dart';
|
import 'package:immich_mobile/providers/map/map_service.provider.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||||
@ -6,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'map_marker.provider.g.dart';
|
part 'map_marker.provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<MapMarker>> mapMarkers(MapMarkersRef ref) async {
|
Future<List<MapMarker>> mapMarkers(Ref ref) async {
|
||||||
final service = ref.read(mapServiceProvider);
|
final service = ref.read(mapServiceProvider);
|
||||||
final mapState = ref.read(mapStateNotifierProvider);
|
final mapState = ref.read(mapStateNotifierProvider);
|
||||||
DateTime? fileCreatedAfter;
|
DateTime? fileCreatedAfter;
|
||||||
|
@ -19,6 +19,8 @@ final mapMarkersProvider = AutoDisposeFutureProvider<List<MapMarker>>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef MapMarkersRef = AutoDisposeFutureProviderRef<List<MapMarker>>;
|
typedef MapMarkersRef = AutoDisposeFutureProviderRef<List<MapMarker>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/services/map.service.dart';
|
import 'package:immich_mobile/services/map.service.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@ -5,5 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'map_service.provider.g.dart';
|
part 'map_service.provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
MapSerivce mapService(MapServiceRef ref) =>
|
MapSerivce mapService(Ref ref) => MapSerivce(ref.watch(apiServiceProvider));
|
||||||
MapSerivce(ref.watch(apiServiceProvider));
|
|
||||||
|
@ -19,6 +19,8 @@ final mapServiceProvider = AutoDisposeProvider<MapSerivce>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef MapServiceRef = AutoDisposeProviderRef<MapSerivce>;
|
typedef MapServiceRef = AutoDisposeProviderRef<MapSerivce>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -23,4 +23,4 @@ final mapStateNotifierProvider =
|
|||||||
|
|
||||||
typedef _$MapStateNotifier = Notifier<MapState>;
|
typedef _$MapStateNotifier = Notifier<MapState>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -45,7 +45,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<RenderList> paginatedSearchRenderList(
|
Future<RenderList> paginatedSearchRenderList(
|
||||||
PaginatedSearchRenderListRef ref,
|
Ref ref,
|
||||||
) {
|
) {
|
||||||
final result = ref.watch(paginatedSearchProvider);
|
final result = ref.watch(paginatedSearchProvider);
|
||||||
final timelineService = ref.watch(timelineServiceProvider);
|
final timelineService = ref.watch(timelineServiceProvider);
|
||||||
|
@ -22,6 +22,8 @@ final paginatedSearchRenderListProvider =
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef<RenderList>;
|
typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef<RenderList>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/interfaces/person_api.interface.dart';
|
import 'package:immich_mobile/interfaces/person_api.interface.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/services/person.service.dart';
|
import 'package:immich_mobile/services/person.service.dart';
|
||||||
@ -9,7 +10,7 @@ part 'people.provider.g.dart';
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<Person>> getAllPeople(
|
Future<List<Person>> getAllPeople(
|
||||||
GetAllPeopleRef ref,
|
Ref ref,
|
||||||
) async {
|
) async {
|
||||||
final PersonService personService = ref.read(personServiceProvider);
|
final PersonService personService = ref.read(personServiceProvider);
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ Future<List<Person>> getAllPeople(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
Future<RenderList> personAssets(Ref ref, String personId) async {
|
||||||
final PersonService personService = ref.read(personServiceProvider);
|
final PersonService personService = ref.read(personServiceProvider);
|
||||||
final assets = await personService.getPersonAssets(personId);
|
final assets = await personService.getPersonAssets(personId);
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<bool> updatePersonName(
|
Future<bool> updatePersonName(
|
||||||
UpdatePersonNameRef ref,
|
Ref ref,
|
||||||
String personId,
|
String personId,
|
||||||
String updatedName,
|
String updatedName,
|
||||||
) async {
|
) async {
|
||||||
|
@ -19,6 +19,8 @@ final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>;
|
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>;
|
||||||
String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832';
|
String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832';
|
||||||
|
|
||||||
@ -156,6 +158,8 @@ class PersonAssetsProvider extends AutoDisposeFutureProvider<RenderList> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin PersonAssetsRef on AutoDisposeFutureProviderRef<RenderList> {
|
mixin PersonAssetsRef on AutoDisposeFutureProviderRef<RenderList> {
|
||||||
/// The parameter `personId` of this provider.
|
/// The parameter `personId` of this provider.
|
||||||
String get personId;
|
String get personId;
|
||||||
@ -296,6 +300,8 @@ class UpdatePersonNameProvider extends AutoDisposeFutureProvider<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef<bool> {
|
mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef<bool> {
|
||||||
/// The parameter `personId` of this provider.
|
/// The parameter `personId` of this provider.
|
||||||
String get personId;
|
String get personId;
|
||||||
@ -314,4 +320,4 @@ class _UpdatePersonNameProviderElement
|
|||||||
String get updatedName => (origin as UpdatePersonNameProvider).updatedName;
|
String get updatedName => (origin as UpdatePersonNameProvider).updatedName;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/services/search.service.dart';
|
import 'package:immich_mobile/services/search.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@ -6,7 +7,7 @@ part 'search_filter.provider.g.dart';
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<String>> getSearchSuggestions(
|
Future<List<String>> getSearchSuggestions(
|
||||||
GetSearchSuggestionsRef ref,
|
Ref ref,
|
||||||
SearchSuggestionType type, {
|
SearchSuggestionType type, {
|
||||||
String? locationCountry,
|
String? locationCountry,
|
||||||
String? locationState,
|
String? locationState,
|
||||||
|
@ -189,6 +189,8 @@ class GetSearchSuggestionsProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef<List<String>> {
|
mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef<List<String>> {
|
||||||
/// The parameter `type` of this provider.
|
/// The parameter `type` of this provider.
|
||||||
SearchSuggestionType get type;
|
SearchSuggestionType get type;
|
||||||
@ -226,4 +228,4 @@ class _GetSearchSuggestionsProviderElement
|
|||||||
String? get model => (origin as GetSearchSuggestionsProvider).model;
|
String? get model => (origin as GetSearchSuggestionsProvider).model;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -788,6 +788,53 @@ class FilterImageRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [FolderPage]
|
||||||
|
class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
|
||||||
|
FolderRoute({
|
||||||
|
Key? key,
|
||||||
|
RecursiveFolder? folder,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
FolderRoute.name,
|
||||||
|
args: FolderRouteArgs(
|
||||||
|
key: key,
|
||||||
|
folder: folder,
|
||||||
|
),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'FolderRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args =
|
||||||
|
data.argsAs<FolderRouteArgs>(orElse: () => const FolderRouteArgs());
|
||||||
|
return FolderPage(
|
||||||
|
key: args.key,
|
||||||
|
folder: args.folder,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FolderRouteArgs {
|
||||||
|
const FolderRouteArgs({
|
||||||
|
this.key,
|
||||||
|
this.folder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final RecursiveFolder? folder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FolderRouteArgs{key: $key, folder: $folder}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [GalleryViewerPage]
|
/// [GalleryViewerPage]
|
||||||
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||||
@ -1175,40 +1222,6 @@ class PartnerRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// manually written (with love) route for
|
|
||||||
/// [FolderPage]
|
|
||||||
class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
|
|
||||||
FolderRoute({
|
|
||||||
RecursiveFolder? folder,
|
|
||||||
List<PageRouteInfo>? children,
|
|
||||||
}) : super(
|
|
||||||
FolderRoute.name,
|
|
||||||
args: FolderRouteArgs(folder: folder),
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'FolderRoute';
|
|
||||||
|
|
||||||
static PageInfo page = PageInfo(
|
|
||||||
name,
|
|
||||||
builder: (data) {
|
|
||||||
final args = data.argsAs<FolderRouteArgs>();
|
|
||||||
return FolderPage(folder: args.folder);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class FolderRouteArgs {
|
|
||||||
const FolderRouteArgs({this.folder});
|
|
||||||
|
|
||||||
final RecursiveFolder? folder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'FolderRouteArgs{folder: $folder}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [PeopleCollectionPage]
|
/// [PeopleCollectionPage]
|
||||||
class PeopleCollectionRoute extends PageRouteInfo<void> {
|
class PeopleCollectionRoute extends PageRouteInfo<void> {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:flutter_web_auth/flutter_web_auth.dart';
|
|
||||||
|
|
||||||
// Redirect URL = app.immich:///oauth-callback
|
// Redirect URL = app.immich:///oauth-callback
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class OAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<LoginResponseDto?> oAuthLogin(String oauthUrl) async {
|
Future<LoginResponseDto?> oAuthLogin(String oauthUrl) async {
|
||||||
String result = await FlutterWebAuth.authenticate(
|
String result = await FlutterWebAuth2.authenticate(
|
||||||
url: oauthUrl,
|
url: oauthUrl,
|
||||||
callbackUrlScheme: callbackUrlScheme,
|
callbackUrlScheme: callbackUrlScheme,
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||||
@ -11,7 +12,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'person.service.g.dart';
|
part 'person.service.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
PersonService personService(PersonServiceRef ref) => PersonService(
|
PersonService personService(Ref ref) => PersonService(
|
||||||
ref.watch(personApiRepositoryProvider),
|
ref.watch(personApiRepositoryProvider),
|
||||||
ref.watch(assetApiRepositoryProvider),
|
ref.watch(assetApiRepositoryProvider),
|
||||||
ref.read(assetRepositoryProvider),
|
ref.read(assetRepositoryProvider),
|
||||||
|
4
mobile/lib/services/person.service.g.dart
generated
4
mobile/lib/services/person.service.g.dart
generated
@ -20,6 +20,8 @@ final personServiceProvider = AutoDisposeProvider<PersonService>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef PersonServiceRef = AutoDisposeProviderRef<PersonService>;
|
typedef PersonServiceRef = AutoDisposeProviderRef<PersonService>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
@ -332,7 +332,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index != -1 && index < widget.renderList.elements.length) {
|
if (index < widget.renderList.elements.length) {
|
||||||
// Not sure why the index is shifted, but it works. :3
|
// Not sure why the index is shifted, but it works. :3
|
||||||
_scrollToIndex(index + 1);
|
_scrollToIndex(index + 1);
|
||||||
} else {
|
} else {
|
||||||
|
@ -44,7 +44,19 @@ class PeopleInfo extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final curatedPeople = people
|
final curatedPeople = people
|
||||||
?.map((p) => SearchCuratedContent(id: p.id, label: p.name))
|
?.map(
|
||||||
|
(p) => SearchCuratedContent(
|
||||||
|
id: p.id,
|
||||||
|
label: p.name,
|
||||||
|
subtitle: p.birthDate != null
|
||||||
|
? "exif_bottom_sheet_person_age".tr(
|
||||||
|
args: [
|
||||||
|
_calculateAge(p.birthDate!).toString(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
@ -99,4 +111,17 @@ class PeopleInfo extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _calculateAge(DateTime birthDate) {
|
||||||
|
DateTime today = DateTime.now();
|
||||||
|
int age = today.year - birthDate.year;
|
||||||
|
|
||||||
|
// Check if the birthday has occurred this year
|
||||||
|
if (today.month < birthDate.month ||
|
||||||
|
(today.month == birthDate.month && today.day < birthDate.day)) {
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return age;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
|
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||||
@ -95,6 +97,16 @@ class GalleryAppBar extends ConsumerWidget {
|
|||||||
ref.read(downloadStateProvider.notifier).downloadAsset(asset, context);
|
ref.read(downloadStateProvider.notifier).downloadAsset(asset, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLocateAsset() async {
|
||||||
|
// Go back to the gallery
|
||||||
|
await context.maybePop();
|
||||||
|
await context
|
||||||
|
.navigateTo(const TabControllerRoute(children: [PhotosRoute()]));
|
||||||
|
ref.read(tabProvider.notifier).update((state) => state = TabEnum.home);
|
||||||
|
// Scroll to the asset's date
|
||||||
|
scrollToDateNotifierProvider.scrollToDate(asset.fileCreatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !showControls,
|
ignoring: !showControls,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
@ -107,6 +119,7 @@ class GalleryAppBar extends ConsumerWidget {
|
|||||||
isPartner: isPartner,
|
isPartner: isPartner,
|
||||||
asset: asset,
|
asset: asset,
|
||||||
onMoreInfoPressed: showInfo,
|
onMoreInfoPressed: showInfo,
|
||||||
|
onLocatePressed: handleLocateAsset,
|
||||||
onFavorite: toggleFavorite,
|
onFavorite: toggleFavorite,
|
||||||
onRestorePressed: () => handleRestore(asset),
|
onRestorePressed: () => handleRestore(asset),
|
||||||
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
|
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
|
||||||
|
@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends HookConsumerWidget {
|
class TopControlAppBar extends HookConsumerWidget {
|
||||||
@ -13,6 +14,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
required this.asset,
|
required this.asset,
|
||||||
required this.onMoreInfoPressed,
|
required this.onMoreInfoPressed,
|
||||||
required this.onDownloadPressed,
|
required this.onDownloadPressed,
|
||||||
|
required this.onLocatePressed,
|
||||||
required this.onAddToAlbumPressed,
|
required this.onAddToAlbumPressed,
|
||||||
required this.onRestorePressed,
|
required this.onRestorePressed,
|
||||||
required this.onFavorite,
|
required this.onFavorite,
|
||||||
@ -26,6 +28,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final VoidCallback? onUploadPressed;
|
final VoidCallback? onUploadPressed;
|
||||||
final VoidCallback? onDownloadPressed;
|
final VoidCallback? onDownloadPressed;
|
||||||
|
final VoidCallback onLocatePressed;
|
||||||
final VoidCallback onAddToAlbumPressed;
|
final VoidCallback onAddToAlbumPressed;
|
||||||
final VoidCallback onRestorePressed;
|
final VoidCallback onRestorePressed;
|
||||||
final VoidCallback onActivitiesPressed;
|
final VoidCallback onActivitiesPressed;
|
||||||
@ -54,6 +57,18 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildLocateButton() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
onLocatePressed();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.image_search,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildMoreInfoButton() {
|
Widget buildMoreInfoButton() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -159,6 +174,8 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
shape: const Border(),
|
shape: const Border(),
|
||||||
actions: [
|
actions: [
|
||||||
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
||||||
|
if (isOwner && ref.read(tabProvider.notifier).state != TabEnum.home)
|
||||||
|
buildLocateButton(),
|
||||||
if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
|
if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
|
||||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||||
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
||||||
|
@ -86,12 +86,22 @@ class CuratedPeopleRow extends StatelessWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Text(
|
return Column(
|
||||||
person.label,
|
mainAxisSize: MainAxisSize.min,
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
overflow: TextOverflow.ellipsis,
|
Text(
|
||||||
style: context.textTheme.labelLarge,
|
person.label,
|
||||||
maxLines: 2,
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
if (person.subtitle != null)
|
||||||
|
Text(
|
||||||
|
person.subtitle!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,6 +358,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.10"
|
||||||
|
desktop_webview_window:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: desktop_webview_window
|
||||||
|
sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.3"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -508,10 +516,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_hooks
|
name: flutter_hooks
|
||||||
sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70
|
sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.5"
|
version: "0.21.2"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -577,10 +585,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
|
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.1"
|
version: "2.6.1"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -602,14 +610,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
flutter_web_auth:
|
flutter_web_auth_2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_web_auth
|
name: flutter_web_auth_2
|
||||||
sha256: "95e4856e24fb6ac1678f5ff334743b63f782d839ab324543d29ccbd295176209"
|
sha256: "561c32d32ed537853de43852c35849cf1d37f3482f41f22b718ab6112f96b333"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "5.0.0-alpha.0"
|
||||||
|
flutter_web_auth_2_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_web_auth_2_platform_interface
|
||||||
|
sha256: "45927587ebb2364cd273675ec95f6f67b81725754b416cef2b65cdc63fd3e853"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0-alpha.0"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -712,10 +728,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hooks_riverpod
|
name: hooks_riverpod
|
||||||
sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a"
|
sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.1"
|
version: "2.6.1"
|
||||||
hotreloader:
|
hotreloader:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1276,42 +1292,42 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod
|
name: riverpod
|
||||||
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
|
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.1"
|
version: "2.6.1"
|
||||||
riverpod_analyzer_utils:
|
riverpod_analyzer_utils:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod_analyzer_utils
|
name: riverpod_analyzer_utils
|
||||||
sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d
|
sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.3"
|
version: "0.5.6"
|
||||||
riverpod_annotation:
|
riverpod_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: riverpod_annotation
|
name: riverpod_annotation
|
||||||
sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c
|
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.5"
|
version: "2.6.1"
|
||||||
riverpod_generator:
|
riverpod_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_generator
|
name: riverpod_generator
|
||||||
sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f"
|
sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.6.1"
|
||||||
riverpod_lint:
|
riverpod_lint:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_lint
|
name: riverpod_lint
|
||||||
sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7
|
sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.12"
|
version: "2.6.1"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1813,6 +1829,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.4"
|
version: "1.1.4"
|
||||||
|
window_to_front:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: window_to_front
|
||||||
|
sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.3"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1846,5 +1870,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.5.3 <4.0.0"
|
dart: ">=3.5.0 <4.0.0"
|
||||||
flutter: ">=3.24.5"
|
flutter: ">=3.24.5"
|
||||||
|
@ -17,9 +17,9 @@ dependencies:
|
|||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
photo_manager: ^3.6.1
|
photo_manager: ^3.6.1
|
||||||
photo_manager_image_provider: ^2.2.0
|
photo_manager_image_provider: ^2.2.0
|
||||||
flutter_hooks: ^0.20.4
|
flutter_hooks: ^0.21.2
|
||||||
hooks_riverpod: ^2.4.9
|
hooks_riverpod: ^2.6.1
|
||||||
riverpod_annotation: ^2.3.3
|
riverpod_annotation: ^2.6.1
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
flutter_cache_manager: ^3.3.1
|
flutter_cache_manager: ^3.3.1
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
@ -42,7 +42,7 @@ dependencies:
|
|||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
http_parser: ^4.0.2
|
http_parser: ^4.0.2
|
||||||
flutter_web_auth: 0.6.0
|
flutter_web_auth_2: ^5.0.0-alpha.0
|
||||||
easy_image_viewer: ^1.4.0
|
easy_image_viewer: ^1.4.0
|
||||||
isar:
|
isar:
|
||||||
version: *isar_version
|
version: *isar_version
|
||||||
@ -108,8 +108,8 @@ dev_dependencies:
|
|||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
custom_lint: ^0.6.4
|
custom_lint: ^0.6.4
|
||||||
riverpod_lint: ^2.3.7
|
riverpod_lint: ^2.6.1
|
||||||
riverpod_generator: ^2.3.9
|
riverpod_generator: ^2.6.1
|
||||||
mocktail: ^1.0.3
|
mocktail: ^1.0.3
|
||||||
immich_mobile_immich_lint:
|
immich_mobile_immich_lint:
|
||||||
path: './immich_lint'
|
path: './immich_lint'
|
||||||
|
39
server/package-lock.json
generated
39
server/package-lock.json
generated
@ -32,7 +32,8 @@
|
|||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie": "^1.0.2",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
@ -81,8 +82,7 @@
|
|||||||
"@types/archiver": "^6.0.0",
|
"@types/archiver": "^6.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
@ -5612,13 +5612,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/cookie": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/cookie-parser": {
|
"node_modules/@types/cookie-parser": {
|
||||||
"version": "1.4.8",
|
"version": "1.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
|
||||||
@ -8071,12 +8064,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie-parser": {
|
"node_modules/cookie-parser": {
|
||||||
@ -8092,6 +8085,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie-parser/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cookie-signature": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
@ -8728,6 +8730,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/engine.io/node_modules/debug": {
|
"node_modules/engine.io/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
@ -58,7 +58,8 @@
|
|||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie": "^1.0.2",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
@ -107,8 +108,7 @@
|
|||||||
"@types/archiver": "^6.0.0",
|
"@types/archiver": "^6.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
@ -10,6 +10,20 @@ export type AuthUser = {
|
|||||||
quotaSizeInBytes: number | null;
|
quotaSizeInBytes: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Library = {
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
updateId: string;
|
||||||
|
name: string;
|
||||||
|
importPaths: string[];
|
||||||
|
exclusionPatterns: string[];
|
||||||
|
deletedAt: Date | null;
|
||||||
|
refreshedAt: Date | null;
|
||||||
|
assets?: Asset[];
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthApiKey = {
|
export type AuthApiKey = {
|
||||||
id: string;
|
id: string;
|
||||||
permissions: Permission[];
|
permissions: Permission[];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
|
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
|
||||||
import { LibraryEntity } from 'src/entities/library.entity';
|
import { Library } from 'src/database';
|
||||||
import { Optional, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class CreateLibraryDto {
|
export class CreateLibraryDto {
|
||||||
@ -120,7 +120,7 @@ export class LibraryStatsResponseDto {
|
|||||||
usage = 0;
|
usage = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapLibrary(entity: LibraryEntity): LibraryResponseDto {
|
export function mapLibrary(entity: Library): LibraryResponseDto {
|
||||||
let assetCount = 0;
|
let assetCount = 0;
|
||||||
if (entity.assets) {
|
if (entity.assets) {
|
||||||
assetCount = entity.assets.length;
|
assetCount = entity.assets.length;
|
||||||
|
@ -7,6 +7,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { SourceType } from 'src/enum';
|
import { SourceType } from 'src/enum';
|
||||||
|
import { asDateString } from 'src/utils/date';
|
||||||
import {
|
import {
|
||||||
IsDateStringFormat,
|
IsDateStringFormat,
|
||||||
MaxDateString,
|
MaxDateString,
|
||||||
@ -32,7 +33,7 @@ export class PersonCreateDto {
|
|||||||
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
||||||
@IsDateStringFormat('yyyy-MM-dd')
|
@IsDateStringFormat('yyyy-MM-dd')
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
birthDate?: string | null;
|
birthDate?: Date | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Person visibility
|
* Person visibility
|
||||||
@ -222,7 +223,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
|||||||
return {
|
return {
|
||||||
id: person.id,
|
id: person.id,
|
||||||
name: person.name,
|
name: person.name,
|
||||||
birthDate: person.birthDate,
|
birthDate: asDateString(person.birthDate),
|
||||||
thumbnailPath: person.thumbnailPath,
|
thumbnailPath: person.thumbnailPath,
|
||||||
isHidden: person.isHidden,
|
isHidden: person.isHidden,
|
||||||
isFavorite: person.isFavorite,
|
isFavorite: person.isFavorite,
|
||||||
|
@ -26,7 +26,7 @@ export class LibraryEntity {
|
|||||||
assets!: AssetEntity[];
|
assets!: AssetEntity[];
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||||
owner!: UserEntity;
|
owner?: UserEntity;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
@ -38,7 +38,7 @@ export class PersonEntity {
|
|||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@Column({ type: 'date', nullable: true })
|
@Column({ type: 'date', nullable: true })
|
||||||
birthDate!: string | null;
|
birthDate!: Date | string | null;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
thumbnailPath!: string;
|
thumbnailPath!: string;
|
||||||
|
@ -2,34 +2,7 @@
|
|||||||
|
|
||||||
-- LibraryRepository.get
|
-- LibraryRepository.get
|
||||||
select
|
select
|
||||||
"libraries".*,
|
"libraries".*
|
||||||
(
|
|
||||||
select
|
|
||||||
to_json(obj)
|
|
||||||
from
|
|
||||||
(
|
|
||||||
select
|
|
||||||
"users"."id",
|
|
||||||
"users"."email",
|
|
||||||
"users"."createdAt",
|
|
||||||
"users"."profileImagePath",
|
|
||||||
"users"."isAdmin",
|
|
||||||
"users"."shouldChangePassword",
|
|
||||||
"users"."deletedAt",
|
|
||||||
"users"."oauthId",
|
|
||||||
"users"."updatedAt",
|
|
||||||
"users"."storageLabel",
|
|
||||||
"users"."name",
|
|
||||||
"users"."quotaSizeInBytes",
|
|
||||||
"users"."quotaUsageInBytes",
|
|
||||||
"users"."status",
|
|
||||||
"users"."profileChangedAt"
|
|
||||||
from
|
|
||||||
"users"
|
|
||||||
where
|
|
||||||
"users"."id" = "libraries"."ownerId"
|
|
||||||
) as obj
|
|
||||||
) as "owner"
|
|
||||||
from
|
from
|
||||||
"libraries"
|
"libraries"
|
||||||
where
|
where
|
||||||
@ -38,34 +11,7 @@ where
|
|||||||
|
|
||||||
-- LibraryRepository.getAll
|
-- LibraryRepository.getAll
|
||||||
select
|
select
|
||||||
"libraries".*,
|
"libraries".*
|
||||||
(
|
|
||||||
select
|
|
||||||
to_json(obj)
|
|
||||||
from
|
|
||||||
(
|
|
||||||
select
|
|
||||||
"users"."id",
|
|
||||||
"users"."email",
|
|
||||||
"users"."createdAt",
|
|
||||||
"users"."profileImagePath",
|
|
||||||
"users"."isAdmin",
|
|
||||||
"users"."shouldChangePassword",
|
|
||||||
"users"."deletedAt",
|
|
||||||
"users"."oauthId",
|
|
||||||
"users"."updatedAt",
|
|
||||||
"users"."storageLabel",
|
|
||||||
"users"."name",
|
|
||||||
"users"."quotaSizeInBytes",
|
|
||||||
"users"."quotaUsageInBytes",
|
|
||||||
"users"."status",
|
|
||||||
"users"."profileChangedAt"
|
|
||||||
from
|
|
||||||
"users"
|
|
||||||
where
|
|
||||||
"users"."id" = "libraries"."ownerId"
|
|
||||||
) as obj
|
|
||||||
) as "owner"
|
|
||||||
from
|
from
|
||||||
"libraries"
|
"libraries"
|
||||||
where
|
where
|
||||||
@ -75,34 +21,7 @@ order by
|
|||||||
|
|
||||||
-- LibraryRepository.getAllDeleted
|
-- LibraryRepository.getAllDeleted
|
||||||
select
|
select
|
||||||
"libraries".*,
|
"libraries".*
|
||||||
(
|
|
||||||
select
|
|
||||||
to_json(obj)
|
|
||||||
from
|
|
||||||
(
|
|
||||||
select
|
|
||||||
"users"."id",
|
|
||||||
"users"."email",
|
|
||||||
"users"."createdAt",
|
|
||||||
"users"."profileImagePath",
|
|
||||||
"users"."isAdmin",
|
|
||||||
"users"."shouldChangePassword",
|
|
||||||
"users"."deletedAt",
|
|
||||||
"users"."oauthId",
|
|
||||||
"users"."updatedAt",
|
|
||||||
"users"."storageLabel",
|
|
||||||
"users"."name",
|
|
||||||
"users"."quotaSizeInBytes",
|
|
||||||
"users"."quotaUsageInBytes",
|
|
||||||
"users"."status",
|
|
||||||
"users"."profileChangedAt"
|
|
||||||
from
|
|
||||||
"users"
|
|
||||||
where
|
|
||||||
"users"."id" = "libraries"."ownerId"
|
|
||||||
) as obj
|
|
||||||
) as "owner"
|
|
||||||
from
|
from
|
||||||
"libraries"
|
"libraries"
|
||||||
where
|
where
|
||||||
|
@ -1,31 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { DB, Libraries } from 'src/db';
|
import { DB, Libraries } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||||
import { LibraryEntity } from 'src/entities/library.entity';
|
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType } from 'src/enum';
|
||||||
|
|
||||||
const userColumns = [
|
|
||||||
'users.id',
|
|
||||||
'users.email',
|
|
||||||
'users.createdAt',
|
|
||||||
'users.profileImagePath',
|
|
||||||
'users.isAdmin',
|
|
||||||
'users.shouldChangePassword',
|
|
||||||
'users.deletedAt',
|
|
||||||
'users.oauthId',
|
|
||||||
'users.updatedAt',
|
|
||||||
'users.storageLabel',
|
|
||||||
'users.name',
|
|
||||||
'users.quotaSizeInBytes',
|
|
||||||
'users.quotaUsageInBytes',
|
|
||||||
'users.status',
|
|
||||||
'users.profileChangedAt',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export enum AssetSyncResult {
|
export enum AssetSyncResult {
|
||||||
DO_NOTHING,
|
DO_NOTHING,
|
||||||
UPDATE,
|
UPDATE,
|
||||||
@ -33,72 +13,59 @@ export enum AssetSyncResult {
|
|||||||
CHECK_OFFLINE,
|
CHECK_OFFLINE,
|
||||||
}
|
}
|
||||||
|
|
||||||
const withOwner = (eb: ExpressionBuilder<DB, 'libraries'>) => {
|
|
||||||
return jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'libraries.ownerId').select(userColumns)).as(
|
|
||||||
'owner',
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LibraryRepository {
|
export class LibraryRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
get(id: string, withDeleted = false): Promise<LibraryEntity | undefined> {
|
get(id: string, withDeleted = false) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('libraries')
|
.selectFrom('libraries')
|
||||||
.selectAll('libraries')
|
.selectAll('libraries')
|
||||||
.select(withOwner)
|
|
||||||
.where('libraries.id', '=', id)
|
.where('libraries.id', '=', id)
|
||||||
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
||||||
.executeTakeFirst() as Promise<LibraryEntity | undefined>;
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [] })
|
@GenerateSql({ params: [] })
|
||||||
getAll(withDeleted = false): Promise<LibraryEntity[]> {
|
getAll(withDeleted = false) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('libraries')
|
.selectFrom('libraries')
|
||||||
.selectAll('libraries')
|
.selectAll('libraries')
|
||||||
.select(withOwner)
|
|
||||||
.orderBy('createdAt', 'asc')
|
.orderBy('createdAt', 'asc')
|
||||||
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
||||||
.execute() as unknown as Promise<LibraryEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql()
|
@GenerateSql()
|
||||||
getAllDeleted(): Promise<LibraryEntity[]> {
|
getAllDeleted() {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('libraries')
|
.selectFrom('libraries')
|
||||||
.selectAll('libraries')
|
.selectAll('libraries')
|
||||||
.select(withOwner)
|
|
||||||
.where('libraries.deletedAt', 'is not', null)
|
.where('libraries.deletedAt', 'is not', null)
|
||||||
.orderBy('createdAt', 'asc')
|
.orderBy('createdAt', 'asc')
|
||||||
.execute() as unknown as Promise<LibraryEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
create(library: Insertable<Libraries>): Promise<LibraryEntity> {
|
create(library: Insertable<Libraries>) {
|
||||||
return this.db
|
return this.db.insertInto('libraries').values(library).returningAll().executeTakeFirstOrThrow();
|
||||||
.insertInto('libraries')
|
|
||||||
.values(library)
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirstOrThrow() as Promise<LibraryEntity>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string) {
|
||||||
await this.db.deleteFrom('libraries').where('libraries.id', '=', id).execute();
|
await this.db.deleteFrom('libraries').where('libraries.id', '=', id).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async softDelete(id: string): Promise<void> {
|
async softDelete(id: string) {
|
||||||
await this.db.updateTable('libraries').set({ deletedAt: new Date() }).where('libraries.id', '=', id).execute();
|
await this.db.updateTable('libraries').set({ deletedAt: new Date() }).where('libraries.id', '=', id).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, library: Updateable<Libraries>): Promise<LibraryEntity> {
|
update(id: string, library: Updateable<Libraries>) {
|
||||||
return this.db
|
return this.db
|
||||||
.updateTable('libraries')
|
.updateTable('libraries')
|
||||||
.set(library)
|
.set(library)
|
||||||
.where('libraries.id', '=', id)
|
.where('libraries.id', '=', id)
|
||||||
.returningAll()
|
.returningAll()
|
||||||
.executeTakeFirstOrThrow() as Promise<LibraryEntity>;
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
@ -63,6 +63,14 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
|||||||
Name?: string;
|
Name?: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Device?: {
|
||||||
|
Manufacturer?: string;
|
||||||
|
ModelName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
AndroidMake?: string;
|
||||||
|
AndroidModel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { isString } from 'class-validator';
|
import { isString } from 'class-validator';
|
||||||
import cookieParser from 'cookie';
|
import { parse } from 'cookie';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { IncomingHttpHeaders } from 'node:http';
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
||||||
@ -287,7 +287,7 @@ export class AuthService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCookieToken(headers: IncomingHttpHeaders): string | null {
|
private getCookieToken(headers: IncomingHttpHeaders): string | null {
|
||||||
const cookies = cookieParser.parse(headers.cookie || '');
|
const cookies = parse(headers.cookie || '');
|
||||||
return cookies[ImmichCookie.ACCESS_TOKEN] || null;
|
return cookies[ImmichCookie.ACCESS_TOKEN] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1081,6 +1081,7 @@ describe(MetadataService.name, () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle valid negative rating value', async () => {
|
it('should handle valid negative rating value', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
mockReadTags({ Rating: -1 });
|
mockReadTags({ Rating: -1 });
|
||||||
@ -1193,6 +1194,23 @@ describe(MetadataService.name, () => {
|
|||||||
type: 'VIDEO',
|
type: 'VIDEO',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ Make: '1', Model: '2', Device: { Manufacturer: '3', ModelName: '4' }, AndroidMake: '4', AndroidModel: '5' },
|
||||||
|
{ Device: { Manufacturer: '1', ModelName: '2' }, AndroidMake: '3', AndroidModel: '4' },
|
||||||
|
{ AndroidMake: '1', AndroidModel: '2' },
|
||||||
|
])('should read camera make and model correct place %s', async (metaData) => {
|
||||||
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
mockReadTags(metaData);
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
make: '1',
|
||||||
|
model: '2',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueSidecar', () => {
|
describe('handleQueueSidecar', () => {
|
||||||
|
@ -221,8 +221,8 @@ export class MetadataService extends BaseService {
|
|||||||
colorspace: exifTags.ColorSpace ?? null,
|
colorspace: exifTags.ColorSpace ?? null,
|
||||||
|
|
||||||
// camera
|
// camera
|
||||||
make: exifTags.Make ?? null,
|
make: exifTags.Make ?? exifTags?.Device?.Manufacturer ?? exifTags.AndroidMake ?? null,
|
||||||
model: exifTags.Model ?? null,
|
model: exifTags.Model ?? exifTags?.Device?.ModelName ?? exifTags.AndroidModel ?? null,
|
||||||
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
||||||
iso: validate(exifTags.ISO) as number,
|
iso: validate(exifTags.ISO) as number,
|
||||||
exposureTime: exifTags.ExposureTime ?? null,
|
exposureTime: exifTags.ExposureTime ?? null,
|
||||||
|
@ -222,7 +222,7 @@ describe(PersonService.name, () => {
|
|||||||
mocks.person.update.mockResolvedValue(personStub.withBirthDate);
|
mocks.person.update.mockResolvedValue(personStub.withBirthDate);
|
||||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
|
|
||||||
await expect(sut.update(authStub.admin, 'person-1', { birthDate: '1976-06-30' })).resolves.toEqual({
|
await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({
|
||||||
id: 'person-1',
|
id: 'person-1',
|
||||||
name: 'Person 1',
|
name: 'Person 1',
|
||||||
birthDate: '1976-06-30',
|
birthDate: '1976-06-30',
|
||||||
@ -231,7 +231,7 @@ describe(PersonService.name, () => {
|
|||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
});
|
});
|
||||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' });
|
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
|
3
server/src/utils/date.ts
Normal file
3
server/src/utils/date.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const asDateString = (x: Date | string | null): string | null => {
|
||||||
|
return x instanceof Date ? x.toISOString().split('T')[0] : x;
|
||||||
|
};
|
3
server/test/fixtures/asset.stub.ts
vendored
3
server/test/fixtures/asset.stub.ts
vendored
@ -6,7 +6,6 @@ import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
|||||||
import { StorageAsset } from 'src/types';
|
import { StorageAsset } from 'src/types';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { fileStub } from 'test/fixtures/file.stub';
|
import { fileStub } from 'test/fixtures/file.stub';
|
||||||
import { libraryStub } from 'test/fixtures/library.stub';
|
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
|
|
||||||
const previewFile: AssetFileEntity = {
|
const previewFile: AssetFileEntity = {
|
||||||
@ -396,7 +395,6 @@ export const assetStub = {
|
|||||||
livePhotoVideo: null,
|
livePhotoVideo: null,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
libraryId: 'library-id',
|
libraryId: 'library-id',
|
||||||
library: libraryStub.externalLibrary1,
|
|
||||||
tags: [],
|
tags: [],
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
originalFileName: 'asset-id.jpg',
|
originalFileName: 'asset-id.jpg',
|
||||||
@ -751,7 +749,6 @@ export const assetStub = {
|
|||||||
livePhotoVideo: null,
|
livePhotoVideo: null,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
libraryId: 'library-id',
|
libraryId: 'library-id',
|
||||||
library: libraryStub.externalLibrary1,
|
|
||||||
tags: [],
|
tags: [],
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
originalFileName: 'photo.jpg',
|
originalFileName: 'photo.jpg',
|
||||||
|
77
server/test/fixtures/library.stub.ts
vendored
77
server/test/fixtures/library.stub.ts
vendored
@ -1,77 +0,0 @@
|
|||||||
import { LibraryEntity } from 'src/entities/library.entity';
|
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
|
||||||
|
|
||||||
export const libraryStub = {
|
|
||||||
externalLibrary1: Object.freeze<LibraryEntity>({
|
|
||||||
id: 'library-id',
|
|
||||||
name: 'test_library',
|
|
||||||
assets: [],
|
|
||||||
owner: userStub.admin,
|
|
||||||
ownerId: 'admin_id',
|
|
||||||
importPaths: [],
|
|
||||||
createdAt: new Date('2023-01-01'),
|
|
||||||
updatedAt: new Date('2023-01-01'),
|
|
||||||
refreshedAt: null,
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
externalLibrary2: Object.freeze<LibraryEntity>({
|
|
||||||
id: 'library-id2',
|
|
||||||
name: 'test_library2',
|
|
||||||
assets: [],
|
|
||||||
owner: userStub.admin,
|
|
||||||
ownerId: 'admin_id',
|
|
||||||
importPaths: [],
|
|
||||||
createdAt: new Date('2021-01-01'),
|
|
||||||
updatedAt: new Date('2022-01-01'),
|
|
||||||
refreshedAt: null,
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
externalLibraryWithImportPaths1: Object.freeze<LibraryEntity>({
|
|
||||||
id: 'library-id-with-paths1',
|
|
||||||
name: 'library-with-import-paths1',
|
|
||||||
assets: [],
|
|
||||||
owner: userStub.admin,
|
|
||||||
ownerId: 'admin_id',
|
|
||||||
importPaths: ['/foo', '/bar'],
|
|
||||||
createdAt: new Date('2023-01-01'),
|
|
||||||
updatedAt: new Date('2023-01-01'),
|
|
||||||
refreshedAt: null,
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
externalLibraryWithImportPaths2: Object.freeze<LibraryEntity>({
|
|
||||||
id: 'library-id-with-paths2',
|
|
||||||
name: 'library-with-import-paths2',
|
|
||||||
assets: [],
|
|
||||||
owner: userStub.admin,
|
|
||||||
ownerId: 'admin_id',
|
|
||||||
importPaths: ['/xyz', '/asdf'],
|
|
||||||
createdAt: new Date('2023-01-01'),
|
|
||||||
updatedAt: new Date('2023-01-01'),
|
|
||||||
refreshedAt: null,
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
patternPath: Object.freeze<LibraryEntity>({
|
|
||||||
id: 'library-id1337',
|
|
||||||
name: 'importpath-exclusion-library1',
|
|
||||||
assets: [],
|
|
||||||
owner: userStub.admin,
|
|
||||||
ownerId: 'user-id',
|
|
||||||
importPaths: ['/xyz', '/asdf'],
|
|
||||||
createdAt: new Date('2023-01-01'),
|
|
||||||
updatedAt: new Date('2023-01-01'),
|
|
||||||
refreshedAt: null,
|
|
||||||
exclusionPatterns: ['**/dir1/**'],
|
|
||||||
}),
|
|
||||||
hasImmichPaths: Object.freeze<LibraryEntity>({
|
|
||||||
id: 'library-id1337',
|
|
||||||
name: 'importpath-exclusion-library1',
|
|
||||||
assets: [],
|
|
||||||
owner: userStub.admin,
|
|
||||||
ownerId: 'user-id',
|
|
||||||
importPaths: ['upload/thumbs', 'xyz', 'upload/library'],
|
|
||||||
createdAt: new Date('2023-01-01'),
|
|
||||||
updatedAt: new Date('2023-01-01'),
|
|
||||||
refreshedAt: null,
|
|
||||||
exclusionPatterns: ['**/dir1/**'],
|
|
||||||
}),
|
|
||||||
};
|
|
@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Asset, AuthUser, User } from 'src/database';
|
import { Asset, AuthUser, Library, User } from 'src/database';
|
||||||
import { OnThisDayData } from 'src/entities/memory.entity';
|
import { OnThisDayData } from 'src/entities/memory.entity';
|
||||||
import { AssetStatus, AssetType, MemoryType } from 'src/enum';
|
import { AssetStatus, AssetType, MemoryType } from 'src/enum';
|
||||||
import { ActivityItem, MemoryItem } from 'src/types';
|
import { ActivityItem, MemoryItem } from 'src/types';
|
||||||
@ -13,7 +13,11 @@ export const newDate = () => new Date();
|
|||||||
export const newUpdateId = () => 'uuid-v7';
|
export const newUpdateId = () => 'uuid-v7';
|
||||||
export const newSha1 = () => Buffer.from('this is a fake hash');
|
export const newSha1 = () => Buffer.from('this is a fake hash');
|
||||||
|
|
||||||
const authUser = (authUser: Partial<AuthUser>) => ({
|
const authFactory = (user: Partial<AuthUser> = {}) => ({
|
||||||
|
user: authUserFactory(user),
|
||||||
|
});
|
||||||
|
|
||||||
|
const authUserFactory = (authUser: Partial<AuthUser>) => ({
|
||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
name: 'Test User',
|
name: 'Test User',
|
||||||
@ -23,7 +27,7 @@ const authUser = (authUser: Partial<AuthUser>) => ({
|
|||||||
...authUser,
|
...authUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = (user: Partial<User>) => ({
|
const userFactory = (user: Partial<User> = {}) => ({
|
||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
name: 'Test User',
|
name: 'Test User',
|
||||||
email: 'test@immich.cloud',
|
email: 'test@immich.cloud',
|
||||||
@ -32,75 +36,95 @@ const user = (user: Partial<User>) => ({
|
|||||||
...user,
|
...user,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const factory = {
|
const assetFactory = (asset: Partial<Asset> = {}) => ({
|
||||||
auth: (user: Partial<AuthUser> = {}) => ({
|
id: newUuid(),
|
||||||
user: authUser(user),
|
createdAt: newDate(),
|
||||||
}),
|
updatedAt: newDate(),
|
||||||
authUser,
|
deletedAt: null,
|
||||||
user,
|
updateId: newUpdateId(),
|
||||||
asset: (asset: Partial<Asset> = {}) => ({
|
status: AssetStatus.ACTIVE,
|
||||||
id: newUuid(),
|
checksum: newSha1(),
|
||||||
createdAt: newDate(),
|
deviceAssetId: '',
|
||||||
updatedAt: newDate(),
|
deviceId: '',
|
||||||
deletedAt: null,
|
duplicateId: null,
|
||||||
updateId: newUpdateId(),
|
duration: null,
|
||||||
status: AssetStatus.ACTIVE,
|
encodedVideoPath: null,
|
||||||
checksum: newSha1(),
|
fileCreatedAt: newDate(),
|
||||||
deviceAssetId: '',
|
fileModifiedAt: newDate(),
|
||||||
deviceId: '',
|
isArchived: false,
|
||||||
duplicateId: null,
|
isExternal: false,
|
||||||
duration: null,
|
isFavorite: false,
|
||||||
encodedVideoPath: null,
|
isOffline: false,
|
||||||
fileCreatedAt: newDate(),
|
isVisible: true,
|
||||||
fileModifiedAt: newDate(),
|
libraryId: null,
|
||||||
isArchived: false,
|
livePhotoVideoId: null,
|
||||||
isExternal: false,
|
localDateTime: newDate(),
|
||||||
isFavorite: false,
|
originalFileName: 'IMG_123.jpg',
|
||||||
isOffline: false,
|
originalPath: `upload/12/34/IMG_123.jpg`,
|
||||||
isVisible: true,
|
ownerId: newUuid(),
|
||||||
libraryId: null,
|
sidecarPath: null,
|
||||||
livePhotoVideoId: null,
|
stackId: null,
|
||||||
localDateTime: newDate(),
|
thumbhash: null,
|
||||||
originalFileName: 'IMG_123.jpg',
|
type: AssetType.IMAGE,
|
||||||
originalPath: `upload/12/34/IMG_123.jpg`,
|
...asset,
|
||||||
ownerId: newUuid(),
|
});
|
||||||
sidecarPath: null,
|
|
||||||
stackId: null,
|
const activityFactory = (activity: Partial<ActivityItem> = {}) => {
|
||||||
thumbhash: null,
|
const userId = activity.userId || newUuid();
|
||||||
type: AssetType.IMAGE,
|
return {
|
||||||
...asset,
|
|
||||||
}),
|
|
||||||
activity: (activity: Partial<ActivityItem> = {}) => {
|
|
||||||
const userId = activity.userId || newUuid();
|
|
||||||
return {
|
|
||||||
id: newUuid(),
|
|
||||||
comment: null,
|
|
||||||
isLiked: false,
|
|
||||||
userId,
|
|
||||||
user: user({ id: userId }),
|
|
||||||
assetId: newUuid(),
|
|
||||||
albumId: newUuid(),
|
|
||||||
createdAt: newDate(),
|
|
||||||
updatedAt: newDate(),
|
|
||||||
updateId: newUpdateId(),
|
|
||||||
...activity,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
memory: (memory: Partial<MemoryItem> = {}) => ({
|
|
||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
|
comment: null,
|
||||||
|
isLiked: false,
|
||||||
|
userId,
|
||||||
|
user: userFactory({ id: userId }),
|
||||||
|
assetId: newUuid(),
|
||||||
|
albumId: newUuid(),
|
||||||
createdAt: newDate(),
|
createdAt: newDate(),
|
||||||
updatedAt: newDate(),
|
updatedAt: newDate(),
|
||||||
updateId: newUpdateId(),
|
updateId: newUpdateId(),
|
||||||
deletedAt: null,
|
...activity,
|
||||||
ownerId: newUuid(),
|
};
|
||||||
type: MemoryType.ON_THIS_DAY,
|
};
|
||||||
data: { year: 2024 } as OnThisDayData,
|
|
||||||
isSaved: false,
|
const libraryFactory = (library: Partial<Library> = {}) => ({
|
||||||
memoryAt: newDate(),
|
id: newUuid(),
|
||||||
seenAt: null,
|
createdAt: newDate(),
|
||||||
showAt: newDate(),
|
updatedAt: newDate(),
|
||||||
hideAt: newDate(),
|
updateId: newUpdateId(),
|
||||||
assets: [],
|
deletedAt: null,
|
||||||
...memory,
|
refreshedAt: null,
|
||||||
}),
|
name: 'Library',
|
||||||
|
assets: [],
|
||||||
|
ownerId: newUuid(),
|
||||||
|
importPaths: [],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
...library,
|
||||||
|
});
|
||||||
|
|
||||||
|
const memoryFactory = (memory: Partial<MemoryItem> = {}) => ({
|
||||||
|
id: newUuid(),
|
||||||
|
createdAt: newDate(),
|
||||||
|
updatedAt: newDate(),
|
||||||
|
updateId: newUpdateId(),
|
||||||
|
deletedAt: null,
|
||||||
|
ownerId: newUuid(),
|
||||||
|
type: MemoryType.ON_THIS_DAY,
|
||||||
|
data: { year: 2024 } as OnThisDayData,
|
||||||
|
isSaved: false,
|
||||||
|
memoryAt: newDate(),
|
||||||
|
seenAt: null,
|
||||||
|
showAt: newDate(),
|
||||||
|
hideAt: newDate(),
|
||||||
|
assets: [],
|
||||||
|
...memory,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const factory = {
|
||||||
|
activity: activityFactory,
|
||||||
|
asset: assetFactory,
|
||||||
|
auth: authFactory,
|
||||||
|
authUser: authUserFactory,
|
||||||
|
library: libraryFactory,
|
||||||
|
memory: memoryFactory,
|
||||||
|
user: userFactory,
|
||||||
};
|
};
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
widthStyle="100%"
|
widthStyle="100%"
|
||||||
/>
|
/>
|
||||||
{#if person.isFavorite}
|
{#if person.isFavorite}
|
||||||
<div class="absolute top-2 left-2">
|
<div class="absolute top-4 left-4">
|
||||||
<Icon path={mdiHeart} size="24" class="text-white" />
|
<Icon path={mdiHeart} size="24" class="text-white" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -50,9 +50,11 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
||||||
|
title={$t('previous')}
|
||||||
|
aria-label={$t('previous')}
|
||||||
onclick={scrollLeft}
|
onclick={scrollLeft}
|
||||||
>
|
>
|
||||||
<Icon path={mdiChevronLeft} size="36" /></button
|
<Icon path={mdiChevronLeft} size="36" ariaLabel={$t('previous')} /></button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -61,9 +63,11 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
||||||
|
title={$t('next')}
|
||||||
|
aria-label={$t('next')}
|
||||||
onclick={scrollRight}
|
onclick={scrollRight}
|
||||||
>
|
>
|
||||||
<Icon path={mdiChevronRight} size="36" /></button
|
<Icon path={mdiChevronRight} size="36" ariaLabel={$t('next')} /></button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
let showFilter = $state(false);
|
let showFilter = $state(false);
|
||||||
let isSearchSuggestions = $state(false);
|
let isSearchSuggestions = $state(false);
|
||||||
let selectedId: string | undefined = $state();
|
let selectedId: string | undefined = $state();
|
||||||
|
let isFocus = $state(false);
|
||||||
|
|
||||||
const listboxId = generateId();
|
const listboxId = generateId();
|
||||||
|
|
||||||
@ -98,7 +99,25 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
handlePromiseError(handleSearch({ query: value }));
|
const searchType = getSearchType();
|
||||||
|
let payload: SmartSearchDto | MetadataSearchDto = {} as SmartSearchDto | MetadataSearchDto;
|
||||||
|
|
||||||
|
switch (searchType) {
|
||||||
|
case 'smart': {
|
||||||
|
payload = { query: value } as SmartSearchDto;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'metadata': {
|
||||||
|
payload = { originalFileName: value } as MetadataSearchDto;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'description': {
|
||||||
|
payload = { description: value } as MetadataSearchDto;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePromiseError(handleSearch(payload));
|
||||||
saveSearchTerm(value);
|
saveSearchTerm(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,10 +151,12 @@
|
|||||||
|
|
||||||
const openDropdown = () => {
|
const openDropdown = () => {
|
||||||
showSuggestions = true;
|
showSuggestions = true;
|
||||||
|
isFocus = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDropdown = () => {
|
const closeDropdown = () => {
|
||||||
showSuggestions = false;
|
showSuggestions = false;
|
||||||
|
isFocus = false;
|
||||||
searchHistoryBox?.clearSelection();
|
searchHistoryBox?.clearSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,6 +164,26 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSubmit();
|
onSubmit();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getSearchType(): 'smart' | 'metadata' | 'description' {
|
||||||
|
const t = localStorage.getItem('searchQueryType');
|
||||||
|
return t === 'smart' || t === 'description' ? t : 'metadata';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchTypeText(): string {
|
||||||
|
const searchType = getSearchType();
|
||||||
|
switch (searchType) {
|
||||||
|
case 'smart': {
|
||||||
|
return $t('context');
|
||||||
|
}
|
||||||
|
case 'metadata': {
|
||||||
|
return $t('filename');
|
||||||
|
}
|
||||||
|
case 'description': {
|
||||||
|
return $t('description');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
@ -214,6 +255,21 @@
|
|||||||
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all">
|
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all">
|
||||||
<CircleIconButton title={$t('show_search_options')} icon={mdiTune} onclick={onFilterClick} size="20" />
|
<CircleIconButton title={$t('show_search_options')} icon={mdiTune} onclick={onFilterClick} size="20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if isFocus}
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 flex items-center"
|
||||||
|
class:right-16={isFocus}
|
||||||
|
class:right-28={isFocus && value.length > 0}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="bg-immich-primary text-white dark:bg-immich-dark-primary/90 dark:text-black/75 rounded-full px-3 py-1 text-xs z-10"
|
||||||
|
>
|
||||||
|
{getSearchTypeText()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showClearIcon}
|
{#if showClearIcon}
|
||||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2">
|
<div class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<CircleIconButton onclick={onClear} icon={mdiClose} title={$t('clear')} size="20" />
|
<CircleIconButton onclick={onClear} icon={mdiClose} title={$t('clear')} size="20" />
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import type { SearchLocationFilter } from './search-location-section.svelte';
|
import type { SearchLocationFilter } from './search-location-section.svelte';
|
||||||
import type { SearchDisplayFilters } from './search-display-section.svelte';
|
import type { SearchDisplayFilters } from './search-display-section.svelte';
|
||||||
import type { SearchDateFilter } from './search-date-section.svelte';
|
import type { SearchDateFilter } from './search-date-section.svelte';
|
||||||
import { MediaType } from '$lib/constants';
|
import { MediaType, QueryType, validQueryTypes } from '$lib/constants';
|
||||||
|
|
||||||
export type SearchFilter = {
|
export type SearchFilter = {
|
||||||
query: string;
|
query: string;
|
||||||
@ -55,9 +55,18 @@
|
|||||||
return value === null ? undefined : value;
|
return value === null ? undefined : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function storeQueryType(type: SearchFilter['queryType']) {
|
||||||
|
localStorage.setItem('searchQueryType', type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultQueryType(): QueryType {
|
||||||
|
const storedQueryType = localStorage.getItem('searchQueryType') as QueryType;
|
||||||
|
return validQueryTypes.has(storedQueryType) ? storedQueryType : QueryType.SMART;
|
||||||
|
}
|
||||||
|
|
||||||
let filter: SearchFilter = $state({
|
let filter: SearchFilter = $state({
|
||||||
query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '',
|
query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '',
|
||||||
queryType: 'smart',
|
queryType: defaultQueryType(),
|
||||||
personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []),
|
personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []),
|
||||||
tagIds: new SvelteSet('tagIds' in searchQuery ? searchQuery.tagIds : []),
|
tagIds: new SvelteSet('tagIds' in searchQuery ? searchQuery.tagIds : []),
|
||||||
location: {
|
location: {
|
||||||
@ -90,7 +99,7 @@
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
filter = {
|
filter = {
|
||||||
query: '',
|
query: '',
|
||||||
queryType: 'smart',
|
queryType: defaultQueryType(), // retain from localStorage or default
|
||||||
personIds: new SvelteSet(),
|
personIds: new SvelteSet(),
|
||||||
tagIds: new SvelteSet(),
|
tagIds: new SvelteSet(),
|
||||||
location: {},
|
location: {},
|
||||||
@ -142,8 +151,14 @@
|
|||||||
|
|
||||||
const onsubmit = (event: Event) => {
|
const onsubmit = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
storeQueryType(filter.queryType);
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Will be called whenever queryType changes, not just onsubmit.
|
||||||
|
$effect(() => {
|
||||||
|
storeQueryType(filter.queryType);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}>
|
<FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}>
|
||||||
|
@ -119,6 +119,14 @@ export const fallbackLocale = {
|
|||||||
name: 'English (US)',
|
name: 'English (US)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum QueryType {
|
||||||
|
SMART = 'smart',
|
||||||
|
METADATA = 'metadata',
|
||||||
|
DESCRIPTION = 'description',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validQueryTypes = new Set([QueryType.SMART, QueryType.METADATA, QueryType.DESCRIPTION]);
|
||||||
|
|
||||||
export const locales = [
|
export const locales = [
|
||||||
{ code: 'af-ZA', name: 'Afrikaans (South Africa)' },
|
{ code: 'af-ZA', name: 'Afrikaans (South Africa)' },
|
||||||
{ code: 'sq-AL', name: 'Albanian (Albania)' },
|
{ code: 'sq-AL', name: 'Albanian (Albania)' },
|
||||||
|
Reference in New Issue
Block a user