mirror of
https://github.com/immich-app/immich.git
synced 2025-03-11 15:09:45 +02:00
Merge remote-tracking branch 'origin/main' into fix-search-scrollbar
This commit is contained in:
commit
c94a47e18e
6
.github/workflows/prepare-release.yml
vendored
6
.github/workflows/prepare-release.yml
vendored
@ -41,8 +41,8 @@ jobs:
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Install Poetry
|
||||
run: pipx install poetry
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Bump version
|
||||
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
|
||||
@ -74,7 +74,7 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
@ -380,27 +380,28 @@ jobs:
|
||||
working-directory: ./machine-learning
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: pipx install poetry
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'poetry'
|
||||
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
|
||||
# with:
|
||||
# python-version: 3.11
|
||||
# cache: 'uv'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with dev --with cpu
|
||||
uv sync --extra cpu
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
poetry run ruff check --output-format=github app export
|
||||
uv run ruff check --output-format=github app export
|
||||
- name: Check black formatting
|
||||
run: |
|
||||
poetry run black --check app export
|
||||
uv run black --check app export
|
||||
- name: Run mypy type checking
|
||||
run: |
|
||||
poetry run mypy --install-types --non-interactive --strict app/
|
||||
uv run mypy --strict app/
|
||||
- name: Run tests and coverage
|
||||
run: |
|
||||
poetry run pytest app --cov=app --cov-report term-missing
|
||||
uv run pytest app --cov=app --cov-report term-missing
|
||||
|
||||
shellcheck:
|
||||
name: ShellCheck
|
||||
|
@ -25,7 +25,7 @@ services:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
target: dev
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
- ../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
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
# 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" \
|
||||
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
|
||||
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
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
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`
|
||||
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>
|
||||
# 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>
|
||||
exit # Exit the Docker shell
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
|
@ -201,7 +201,7 @@ describe('/people', () => {
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
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}`)
|
||||
.send({ birthDate: '1990-01-01' });
|
||||
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 () => {
|
||||
|
@ -19,20 +19,16 @@ FROM builder-${DEVICE} AS builder
|
||||
|
||||
ARG DEVICE
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=true \
|
||||
VIRTUAL_ENV="/opt/venv" \
|
||||
PATH="/opt/venv/bin:${PATH}"
|
||||
PYTHONUNBUFFERED=1
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||
|
||||
RUN pip install --upgrade pip && pip install poetry
|
||||
RUN poetry config installer.max-workers 10 && \
|
||||
poetry config virtualenvs.create false
|
||||
RUN python3 -m venv /opt/venv
|
||||
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--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
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu
|
||||
|
||||
@ -93,7 +89,7 @@ WORKDIR /usr/src/app
|
||||
ENV TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PATH="/usr/src/app/.venv/bin:$PATH" \
|
||||
PYTHONPATH=/usr/src \
|
||||
DEVICE=${DEVICE}
|
||||
|
||||
@ -102,7 +98,7 @@ RUN echo "hard core 0" >> /etc/security/limits.conf && \
|
||||
echo "fs.suid_dumpable 0" >> /etc/sysctl.conf && \
|
||||
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 start.sh log_conf.json gunicorn_conf.py ./
|
||||
COPY app .
|
||||
|
@ -5,13 +5,12 @@
|
||||
|
||||
# Setup
|
||||
|
||||
This project uses [Poetry](https://python-poetry.org/docs/#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.
|
||||
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.
|
||||
|
||||
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.
|
||||
This project uses [uv](https://docs.astral.sh/uv/getting-started/installation/), so be sure to install it first.
|
||||
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 `--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 `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
|
||||
|
||||
@ -19,22 +18,25 @@ To measure inference throughput and latency, you can use [Locust](https://locust
|
||||
Locust works by querying the model endpoints and aggregating their statistics, meaning the app must be deployed.
|
||||
You can change the models or adjust options like score thresholds through the Locust UI.
|
||||
|
||||
To get started, you can simply run `locust --web-host 127.0.0.1` and open `localhost:8089` in a browser to access the UI. See the [Locust documentation](https://docs.locust.io/en/stable/index.html) for more info on running Locust.
|
||||
To get started, you can simply run `locust --web-host 127.0.0.1` and open `localhost:8089` in a browser to access the UI. See the [Locust documentation](https://docs.locust.io/en/stable/index.html) for more info on running Locust.
|
||||
|
||||
Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.
|
||||
|
||||
# Facial Recognition
|
||||
|
||||
## 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.
|
||||
|
||||
### Used Models
|
||||
* antelopev2
|
||||
* buffalo_l
|
||||
* buffalo_m
|
||||
* buffalo_s
|
||||
|
||||
- antelopev2
|
||||
- buffalo_l
|
||||
- buffalo_m
|
||||
- buffalo_s
|
||||
|
||||
## 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.
|
||||
|
||||
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"
|
||||
version = "1.129.0"
|
||||
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"
|
||||
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]
|
||||
python = ">=3.10,<4.0"
|
||||
insightface = ">=0.7.3,<1.0"
|
||||
opencv-python-headless = ">=4.7.0.72,<5.0"
|
||||
pillow = ">=9.5.0,<11.0"
|
||||
fastapi = ">=0.95.2,<1.0"
|
||||
uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"}
|
||||
pydantic = "^2.0.0"
|
||||
pydantic-settings = "^2.5.2"
|
||||
aiocache = ">=0.12.1,<1.0"
|
||||
rich = ">=13.4.2"
|
||||
ftfy = ">=6.1.1"
|
||||
python-multipart = ">=0.0.6,<1.0"
|
||||
orjson = ">=3.9.5"
|
||||
gunicorn = ">=21.1.0"
|
||||
huggingface-hub = ">=0.20.1,<1.0"
|
||||
tokenizers = ">=0.15.0,<1.0"
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"httpx>=0.24.1",
|
||||
"pytest>=7.3.1",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"pytest-mock>=3.11.1",
|
||||
]
|
||||
types = [
|
||||
"types-pyyaml>=6.0.12.20241230",
|
||||
"types-requests>=2.32.0.20250306",
|
||||
"types-setuptools>=75.8.2.20250305",
|
||||
"types-simplejson>=3.20.0.20250218",
|
||||
"types-ujson>=5.10.0.20240515",
|
||||
]
|
||||
lint = [
|
||||
"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]
|
||||
mypy = ">=1.3.0"
|
||||
black = ">=23.3.0"
|
||||
pytest = ">=7.3.1"
|
||||
locust = ">=2.15.1"
|
||||
httpx = ">=0.24.1"
|
||||
pytest-asyncio = ">=0.21.0"
|
||||
pytest-cov = ">=4.1.0"
|
||||
ruff = ">=0.0.272"
|
||||
pytest-mock = ">=3.11.1"
|
||||
[project.optional-dependencies]
|
||||
cpu = ["onnxruntime>=1.15.0,<2"]
|
||||
cuda = ["onnxruntime-gpu>=1.17.0,<2"]
|
||||
openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0"]
|
||||
armnn = ["onnxruntime>=1.15.0,<2"]
|
||||
|
||||
[tool.poetry.group.cpu]
|
||||
optional = true
|
||||
[tool.uv]
|
||||
compile-bytecode = true
|
||||
|
||||
[tool.poetry.group.cpu.dependencies]
|
||||
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]]
|
||||
[[tool.uv.index]]
|
||||
name = "cuda12"
|
||||
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]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.mypy]
|
||||
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 e2e version "$SERVER_PUMP"
|
||||
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
|
||||
|
||||
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
|
||||
|
@ -93,9 +93,9 @@
|
||||
|
||||
|
||||
<activity
|
||||
android:name="com.linusu.flutter_web_auth.CallbackActivity"
|
||||
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
|
||||
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" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
@ -264,6 +264,7 @@
|
||||
"exif_bottom_sheet_location_add": "Add a location",
|
||||
"exif_bottom_sheet_people": "PEOPLE",
|
||||
"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_title": "Enable experimental photo grid",
|
||||
"experimental_settings_subtitle": "Use at your own risk!",
|
||||
|
@ -48,7 +48,7 @@ PODS:
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_web_auth (0.6.0):
|
||||
- flutter_web_auth_2 (3.0.0):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
@ -117,7 +117,7 @@ DEPENDENCIES:
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/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`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
@ -166,8 +166,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_udid:
|
||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||
flutter_web_auth:
|
||||
:path: ".symlinks/plugins/flutter_web_auth/ios"
|
||||
flutter_web_auth_2:
|
||||
:path: ".symlinks/plugins/flutter_web_auth_2/ios"
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
geolocator_apple:
|
||||
@ -220,7 +220,7 @@ SPEC CHECKSUMS:
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||
flutter_web_auth: acc15a8fd7bba796a933c724a6dffc3d00f07c27
|
||||
flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
|
@ -8,20 +8,26 @@ class SearchCuratedContent {
|
||||
/// The label to show associated with this curated object
|
||||
final String label;
|
||||
|
||||
/// The subtitle to show below the label
|
||||
final String? subtitle;
|
||||
|
||||
/// The id to lookup the asset from the server
|
||||
final String id;
|
||||
|
||||
SearchCuratedContent({
|
||||
required this.label,
|
||||
required this.id,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
SearchCuratedContent copyWith({
|
||||
String? label,
|
||||
String? subtitle,
|
||||
String? id,
|
||||
}) {
|
||||
return SearchCuratedContent(
|
||||
label: label ?? this.label,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
id: id ?? this.id,
|
||||
);
|
||||
}
|
||||
@ -29,6 +35,7 @@ class SearchCuratedContent {
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'label': label,
|
||||
'subtitle': subtitle,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
@ -36,6 +43,7 @@ class SearchCuratedContent {
|
||||
factory SearchCuratedContent.fromMap(Map<String, dynamic> map) {
|
||||
return SearchCuratedContent(
|
||||
label: map['label'] as String,
|
||||
subtitle: map['subtitle'] as String?,
|
||||
id: map['id'] as String,
|
||||
);
|
||||
}
|
||||
@ -46,13 +54,14 @@ class SearchCuratedContent {
|
||||
SearchCuratedContent.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() => 'CuratedContent(label: $label, id: $id)';
|
||||
String toString() =>
|
||||
'CuratedContent(label: $label, subtitle: $subtitle, id: $id)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant SearchCuratedContent other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.label == label && other.id == id;
|
||||
return other.label == label && other.subtitle == subtitle && other.id == id;
|
||||
}
|
||||
|
||||
@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>> {
|
||||
/// The parameter `albumId` of this provider.
|
||||
String get albumId;
|
||||
@ -206,4 +208,4 @@ class _AlbumActivityProviderElement
|
||||
String? get assetId => (origin as AlbumActivityProvider).assetId;
|
||||
}
|
||||
// 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/services/activity.service.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';
|
||||
|
||||
@riverpod
|
||||
ActivityService activityService(ActivityServiceRef ref) =>
|
||||
ActivityService activityService(Ref ref) =>
|
||||
ActivityService(ref.watch(activityApiRepositoryProvider));
|
||||
|
@ -20,6 +20,8 @@ final activityServiceProvider = AutoDisposeProvider<ActivityService>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>;
|
||||
// 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> {
|
||||
/// The parameter `albumId` of this provider.
|
||||
String get albumId;
|
||||
@ -205,4 +207,4 @@ class _ActivityStatisticsProviderElement
|
||||
String? get assetId => (origin as ActivityStatisticsProvider).assetId;
|
||||
}
|
||||
// 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>;
|
||||
// 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?>;
|
||||
// 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:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'api.provider.g.dart';
|
||||
|
||||
@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,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ApiServiceRef = ProviderRef<ApiService>;
|
||||
// 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:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'app_settings.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppSettingsService appSettingsService(AppSettingsServiceRef ref) =>
|
||||
AppSettingsService();
|
||||
AppSettingsService appSettingsService(Ref ref) => AppSettingsService();
|
||||
|
@ -21,6 +21,8 @@ final appSettingsServiceProvider = Provider<AppSettingsService>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AppSettingsServiceRef = ProviderRef<AppSettingsService>;
|
||||
// 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 ETagService _etagService;
|
||||
final ExifService _exifService;
|
||||
final StateNotifierProviderRef _ref;
|
||||
final Ref _ref;
|
||||
final log = Logger('AssetNotifier');
|
||||
bool _getAllAssetInProgress = 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
|
||||
on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> {
|
||||
/// The parameter `asset` of this provider.
|
||||
@ -186,4 +188,4 @@ class _AssetPeopleNotifierProviderElement
|
||||
Asset get asset => (origin as AssetPeopleNotifierProvider).asset;
|
||||
}
|
||||
// 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
|
||||
int assetStackIndex(AssetStackIndexRef ref, Asset asset) {
|
||||
int assetStackIndex(Ref ref, Asset asset) {
|
||||
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> {
|
||||
/// The parameter `asset` of this provider.
|
||||
Asset get asset;
|
||||
@ -155,4 +157,4 @@ class _AssetStackIndexProviderElement extends AutoDisposeProviderElement<int>
|
||||
Asset get asset => (origin as AssetStackIndexProvider).asset;
|
||||
}
|
||||
// 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?>;
|
||||
// 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>;
|
||||
// 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 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'immich_logo_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<Uint8List> immichLogo(ImmichLogoRef ref) async {
|
||||
Future<Uint8List> immichLogo(Ref ref) async {
|
||||
final json = await rootBundle.loadString('assets/immich-logo.json');
|
||||
final j = jsonDecode(json);
|
||||
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,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ImmichLogoRef = AutoDisposeFutureProviderRef<Uint8List>;
|
||||
// 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:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'db.provider.g.dart';
|
||||
|
||||
@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,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef IsarRef = ProviderRef<Isar>;
|
||||
// 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/infrastructure/repositories/exif.repository.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';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
IExifInfoRepository exifRepository(ExifRepositoryRef ref) =>
|
||||
IExifInfoRepository exifRepository(Ref ref) =>
|
||||
IsarExifRepository(ref.watch(isarProvider));
|
||||
|
@ -20,6 +20,8 @@ final exifRepositoryProvider = Provider<IExifInfoRepository>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ExifRepositoryRef = ProviderRef<IExifInfoRepository>;
|
||||
// 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/infrastructure/repositories/store.repository.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';
|
||||
|
||||
@riverpod
|
||||
IStoreRepository storeRepository(StoreRepositoryRef ref) =>
|
||||
IStoreRepository storeRepository(Ref ref) =>
|
||||
IsarStoreRepository(ref.watch(isarProvider));
|
||||
|
@ -20,6 +20,8 @@ final storeRepositoryProvider = AutoDisposeProvider<IStoreRepository>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>;
|
||||
// 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/providers/map/map_service.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';
|
||||
|
||||
@riverpod
|
||||
Future<List<MapMarker>> mapMarkers(MapMarkersRef ref) async {
|
||||
Future<List<MapMarker>> mapMarkers(Ref ref) async {
|
||||
final service = ref.read(mapServiceProvider);
|
||||
final mapState = ref.read(mapStateNotifierProvider);
|
||||
DateTime? fileCreatedAfter;
|
||||
|
@ -19,6 +19,8 @@ final mapMarkersProvider = AutoDisposeFutureProvider<List<MapMarker>>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef MapMarkersRef = AutoDisposeFutureProviderRef<List<MapMarker>>;
|
||||
// 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/providers/api.provider.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';
|
||||
|
||||
@riverpod
|
||||
MapSerivce mapService(MapServiceRef ref) =>
|
||||
MapSerivce(ref.watch(apiServiceProvider));
|
||||
MapSerivce mapService(Ref ref) => MapSerivce(ref.watch(apiServiceProvider));
|
||||
|
@ -19,6 +19,8 @@ final mapServiceProvider = AutoDisposeProvider<MapSerivce>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef MapServiceRef = AutoDisposeProviderRef<MapSerivce>;
|
||||
// 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>;
|
||||
// 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
|
||||
Future<RenderList> paginatedSearchRenderList(
|
||||
PaginatedSearchRenderListRef ref,
|
||||
Ref ref,
|
||||
) {
|
||||
final result = ref.watch(paginatedSearchProvider);
|
||||
final timelineService = ref.watch(timelineServiceProvider);
|
||||
|
@ -22,6 +22,8 @@ final paginatedSearchRenderListProvider =
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef<RenderList>;
|
||||
// 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/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/services/person.service.dart';
|
||||
@ -9,7 +10,7 @@ part 'people.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<Person>> getAllPeople(
|
||||
GetAllPeopleRef ref,
|
||||
Ref ref,
|
||||
) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
|
||||
@ -19,7 +20,7 @@ Future<List<Person>> getAllPeople(
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
||||
Future<RenderList> personAssets(Ref ref, String personId) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
final assets = await personService.getPersonAssets(personId);
|
||||
|
||||
@ -31,7 +32,7 @@ Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
||||
|
||||
@riverpod
|
||||
Future<bool> updatePersonName(
|
||||
UpdatePersonNameRef ref,
|
||||
Ref ref,
|
||||
String personId,
|
||||
String updatedName,
|
||||
) async {
|
||||
|
@ -19,6 +19,8 @@ final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>;
|
||||
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> {
|
||||
/// The parameter `personId` of this provider.
|
||||
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> {
|
||||
/// The parameter `personId` of this provider.
|
||||
String get personId;
|
||||
@ -314,4 +320,4 @@ class _UpdatePersonNameProviderElement
|
||||
String get updatedName => (origin as UpdatePersonNameProvider).updatedName;
|
||||
}
|
||||
// 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:openapi/api.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@ -6,7 +7,7 @@ part 'search_filter.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<String>> getSearchSuggestions(
|
||||
GetSearchSuggestionsRef ref,
|
||||
Ref ref,
|
||||
SearchSuggestionType type, {
|
||||
String? locationCountry,
|
||||
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>> {
|
||||
/// The parameter `type` of this provider.
|
||||
SearchSuggestionType get type;
|
||||
@ -226,4 +228,4 @@ class _GetSearchSuggestionsProviderElement
|
||||
String? get model => (origin as GetSearchSuggestionsProvider).model;
|
||||
}
|
||||
// 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
|
||||
/// [GalleryViewerPage]
|
||||
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
|
||||
/// [PeopleCollectionPage]
|
||||
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:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:flutter_web_auth/flutter_web_auth.dart';
|
||||
|
||||
// Redirect URL = app.immich:///oauth-callback
|
||||
|
||||
@ -32,7 +32,7 @@ class OAuthService {
|
||||
}
|
||||
|
||||
Future<LoginResponseDto?> oAuthLogin(String oauthUrl) async {
|
||||
String result = await FlutterWebAuth.authenticate(
|
||||
String result = await FlutterWebAuth2.authenticate(
|
||||
url: oauthUrl,
|
||||
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/interfaces/asset.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';
|
||||
|
||||
@riverpod
|
||||
PersonService personService(PersonServiceRef ref) => PersonService(
|
||||
PersonService personService(Ref ref) => PersonService(
|
||||
ref.watch(personApiRepositoryProvider),
|
||||
ref.watch(assetApiRepositoryProvider),
|
||||
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,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef PersonServiceRef = AutoDisposeProviderRef<PersonService>;
|
||||
// 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
|
||||
_scrollToIndex(index + 1);
|
||||
} else {
|
||||
|
@ -44,7 +44,19 @@ class PeopleInfo extends ConsumerWidget {
|
||||
}
|
||||
|
||||
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() ??
|
||||
[];
|
||||
|
||||
@ -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: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/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/providers/asset_viewer/download.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);
|
||||
}
|
||||
|
||||
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(
|
||||
ignoring: !showControls,
|
||||
child: AnimatedOpacity(
|
||||
@ -107,6 +119,7 @@ class GalleryAppBar extends ConsumerWidget {
|
||||
isPartner: isPartner,
|
||||
asset: asset,
|
||||
onMoreInfoPressed: showInfo,
|
||||
onLocatePressed: handleLocateAsset,
|
||||
onFavorite: toggleFavorite,
|
||||
onRestorePressed: () => handleRestore(asset),
|
||||
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/entities/asset.entity.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';
|
||||
|
||||
class TopControlAppBar extends HookConsumerWidget {
|
||||
@ -13,6 +14,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
required this.asset,
|
||||
required this.onMoreInfoPressed,
|
||||
required this.onDownloadPressed,
|
||||
required this.onLocatePressed,
|
||||
required this.onAddToAlbumPressed,
|
||||
required this.onRestorePressed,
|
||||
required this.onFavorite,
|
||||
@ -26,6 +28,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
final Function onMoreInfoPressed;
|
||||
final VoidCallback? onUploadPressed;
|
||||
final VoidCallback? onDownloadPressed;
|
||||
final VoidCallback onLocatePressed;
|
||||
final VoidCallback onAddToAlbumPressed;
|
||||
final VoidCallback onRestorePressed;
|
||||
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() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
@ -159,6 +174,8 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
shape: const Border(),
|
||||
actions: [
|
||||
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
||||
if (isOwner && ref.read(tabProvider.notifier).state != TabEnum.home)
|
||||
buildLocateButton(),
|
||||
if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
|
||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
||||
|
@ -86,12 +86,22 @@ class CuratedPeopleRow extends StatelessWidget {
|
||||
).tr(),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
person.label,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelLarge,
|
||||
maxLines: 2,
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
person.label,
|
||||
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"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -508,10 +516,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_hooks
|
||||
sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70
|
||||
sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.5"
|
||||
version: "0.21.2"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -577,10 +585,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
|
||||
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.6.1"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -602,14 +610,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_web_auth:
|
||||
flutter_web_auth_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_web_auth
|
||||
sha256: "95e4856e24fb6ac1678f5ff334743b63f782d839ab324543d29ccbd295176209"
|
||||
name: flutter_web_auth_2
|
||||
sha256: "561c32d32ed537853de43852c35849cf1d37f3482f41f22b718ab6112f96b333"
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -712,10 +728,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hooks_riverpod
|
||||
sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a"
|
||||
sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.6.1"
|
||||
hotreloader:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1276,42 +1292,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
|
||||
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.6.1"
|
||||
riverpod_analyzer_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod_analyzer_utils
|
||||
sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d
|
||||
sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3"
|
||||
version: "0.5.6"
|
||||
riverpod_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: riverpod_annotation
|
||||
sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c
|
||||
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.6.1"
|
||||
riverpod_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: riverpod_generator
|
||||
sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f"
|
||||
sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.6.1"
|
||||
riverpod_lint:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: riverpod_lint
|
||||
sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7
|
||||
sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.12"
|
||||
version: "2.6.1"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1813,6 +1829,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1846,5 +1870,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.3 <4.0.0"
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.24.5"
|
||||
|
@ -17,9 +17,9 @@ dependencies:
|
||||
path_provider_ios:
|
||||
photo_manager: ^3.6.1
|
||||
photo_manager_image_provider: ^2.2.0
|
||||
flutter_hooks: ^0.20.4
|
||||
hooks_riverpod: ^2.4.9
|
||||
riverpod_annotation: ^2.3.3
|
||||
flutter_hooks: ^0.21.2
|
||||
hooks_riverpod: ^2.6.1
|
||||
riverpod_annotation: ^2.6.1
|
||||
cached_network_image: ^3.3.1
|
||||
flutter_cache_manager: ^3.3.1
|
||||
intl: ^0.19.0
|
||||
@ -42,7 +42,7 @@ dependencies:
|
||||
path_provider: ^2.1.2
|
||||
collection: ^1.18.0
|
||||
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
|
||||
isar:
|
||||
version: *isar_version
|
||||
@ -108,8 +108,8 @@ dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
custom_lint: ^0.6.4
|
||||
riverpod_lint: ^2.3.7
|
||||
riverpod_generator: ^2.3.9
|
||||
riverpod_lint: ^2.6.1
|
||||
riverpod_generator: ^2.6.1
|
||||
mocktail: ^1.0.3
|
||||
immich_mobile_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",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"exiftool-vendored": "^28.3.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
@ -81,8 +82,7 @@
|
||||
"@types/archiver": "^6.0.0",
|
||||
"@types/async-lock": "^1.4.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fluent-ffmpeg": "^2.1.21",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
@ -5612,13 +5612,6 @@
|
||||
"@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": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
|
||||
@ -8071,12 +8064,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
@ -8092,6 +8085,15 @@
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
@ -8728,6 +8730,15 @@
|
||||
"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": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
|
@ -58,7 +58,8 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"exiftool-vendored": "^28.3.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
@ -107,8 +108,7 @@
|
||||
"@types/archiver": "^6.0.0",
|
||||
"@types/async-lock": "^1.4.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fluent-ffmpeg": "^2.1.21",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
|
@ -10,6 +10,20 @@ export type AuthUser = {
|
||||
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 = {
|
||||
id: string;
|
||||
permissions: Permission[];
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
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';
|
||||
|
||||
export class CreateLibraryDto {
|
||||
@ -120,7 +120,7 @@ export class LibraryStatsResponseDto {
|
||||
usage = 0;
|
||||
}
|
||||
|
||||
export function mapLibrary(entity: LibraryEntity): LibraryResponseDto {
|
||||
export function mapLibrary(entity: Library): LibraryResponseDto {
|
||||
let assetCount = 0;
|
||||
if (entity.assets) {
|
||||
assetCount = entity.assets.length;
|
||||
|
@ -7,6 +7,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SourceType } from 'src/enum';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import {
|
||||
IsDateStringFormat,
|
||||
MaxDateString,
|
||||
@ -32,7 +33,7 @@ export class PersonCreateDto {
|
||||
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
||||
@IsDateStringFormat('yyyy-MM-dd')
|
||||
@Optional({ nullable: true })
|
||||
birthDate?: string | null;
|
||||
birthDate?: Date | null;
|
||||
|
||||
/**
|
||||
* Person visibility
|
||||
@ -222,7 +223,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
||||
return {
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
birthDate: person.birthDate,
|
||||
birthDate: asDateString(person.birthDate),
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: person.isHidden,
|
||||
isFavorite: person.isFavorite,
|
||||
|
@ -26,7 +26,7 @@ export class LibraryEntity {
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
owner?: UserEntity;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
@ -38,7 +38,7 @@ export class PersonEntity {
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
birthDate!: string | null;
|
||||
birthDate!: Date | string | null;
|
||||
|
||||
@Column({ default: '' })
|
||||
thumbnailPath!: string;
|
||||
|
@ -2,34 +2,7 @@
|
||||
|
||||
-- LibraryRepository.get
|
||||
select
|
||||
"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"
|
||||
"libraries".*
|
||||
from
|
||||
"libraries"
|
||||
where
|
||||
@ -38,34 +11,7 @@ where
|
||||
|
||||
-- LibraryRepository.getAll
|
||||
select
|
||||
"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"
|
||||
"libraries".*
|
||||
from
|
||||
"libraries"
|
||||
where
|
||||
@ -75,34 +21,7 @@ order by
|
||||
|
||||
-- LibraryRepository.getAllDeleted
|
||||
select
|
||||
"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"
|
||||
"libraries".*
|
||||
from
|
||||
"libraries"
|
||||
where
|
||||
|
@ -1,31 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, Libraries } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
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 {
|
||||
DO_NOTHING,
|
||||
UPDATE,
|
||||
@ -33,72 +13,59 @@ export enum AssetSyncResult {
|
||||
CHECK_OFFLINE,
|
||||
}
|
||||
|
||||
const withOwner = (eb: ExpressionBuilder<DB, 'libraries'>) => {
|
||||
return jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'libraries.ownerId').select(userColumns)).as(
|
||||
'owner',
|
||||
);
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class LibraryRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
get(id: string, withDeleted = false): Promise<LibraryEntity | undefined> {
|
||||
get(id: string, withDeleted = false) {
|
||||
return this.db
|
||||
.selectFrom('libraries')
|
||||
.selectAll('libraries')
|
||||
.select(withOwner)
|
||||
.where('libraries.id', '=', id)
|
||||
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
||||
.executeTakeFirst() as Promise<LibraryEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [] })
|
||||
getAll(withDeleted = false): Promise<LibraryEntity[]> {
|
||||
getAll(withDeleted = false) {
|
||||
return this.db
|
||||
.selectFrom('libraries')
|
||||
.selectAll('libraries')
|
||||
.select(withOwner)
|
||||
.orderBy('createdAt', 'asc')
|
||||
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
||||
.execute() as unknown as Promise<LibraryEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAllDeleted(): Promise<LibraryEntity[]> {
|
||||
getAllDeleted() {
|
||||
return this.db
|
||||
.selectFrom('libraries')
|
||||
.selectAll('libraries')
|
||||
.select(withOwner)
|
||||
.where('libraries.deletedAt', 'is not', null)
|
||||
.orderBy('createdAt', 'asc')
|
||||
.execute() as unknown as Promise<LibraryEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(library: Insertable<Libraries>): Promise<LibraryEntity> {
|
||||
return this.db
|
||||
.insertInto('libraries')
|
||||
.values(library)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<LibraryEntity>;
|
||||
create(library: Insertable<Libraries>) {
|
||||
return this.db.insertInto('libraries').values(library).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
async delete(id: string) {
|
||||
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();
|
||||
}
|
||||
|
||||
update(id: string, library: Updateable<Libraries>): Promise<LibraryEntity> {
|
||||
update(id: string, library: Updateable<Libraries>) {
|
||||
return this.db
|
||||
.updateTable('libraries')
|
||||
.set(library)
|
||||
.where('libraries.id', '=', id)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<LibraryEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
|
@ -63,6 +63,14 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
||||
Name?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
Device?: {
|
||||
Manufacturer?: string;
|
||||
ModelName?: string;
|
||||
};
|
||||
|
||||
AndroidMake?: string;
|
||||
AndroidModel?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { isString } from 'class-validator';
|
||||
import cookieParser from 'cookie';
|
||||
import { parse } from 'cookie';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
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 {
|
||||
const cookies = cookieParser.parse(headers.cookie || '');
|
||||
const cookies = parse(headers.cookie || '');
|
||||
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 () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mockReadTags({ Rating: -1 });
|
||||
@ -1193,6 +1194,23 @@ describe(MetadataService.name, () => {
|
||||
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', () => {
|
||||
|
@ -221,8 +221,8 @@ export class MetadataService extends BaseService {
|
||||
colorspace: exifTags.ColorSpace ?? null,
|
||||
|
||||
// camera
|
||||
make: exifTags.Make ?? null,
|
||||
model: exifTags.Model ?? null,
|
||||
make: exifTags.Make ?? exifTags?.Device?.Manufacturer ?? exifTags.AndroidMake ?? null,
|
||||
model: exifTags.Model ?? exifTags?.Device?.ModelName ?? exifTags.AndroidModel ?? null,
|
||||
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
||||
iso: validate(exifTags.ISO) as number,
|
||||
exposureTime: exifTags.ExposureTime ?? null,
|
||||
|
@ -222,7 +222,7 @@ describe(PersonService.name, () => {
|
||||
mocks.person.update.mockResolvedValue(personStub.withBirthDate);
|
||||
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',
|
||||
name: 'Person 1',
|
||||
birthDate: '1976-06-30',
|
||||
@ -231,7 +231,7 @@ describe(PersonService.name, () => {
|
||||
isFavorite: false,
|
||||
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.queueAll).not.toHaveBeenCalled();
|
||||
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 { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { libraryStub } from 'test/fixtures/library.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
const previewFile: AssetFileEntity = {
|
||||
@ -396,7 +395,6 @@ export const assetStub = {
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: 'library-id',
|
||||
library: libraryStub.externalLibrary1,
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.jpg',
|
||||
@ -751,7 +749,6 @@ export const assetStub = {
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: 'library-id',
|
||||
library: libraryStub.externalLibrary1,
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
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 { Asset, AuthUser, User } from 'src/database';
|
||||
import { Asset, AuthUser, Library, User } from 'src/database';
|
||||
import { OnThisDayData } from 'src/entities/memory.entity';
|
||||
import { AssetStatus, AssetType, MemoryType } from 'src/enum';
|
||||
import { ActivityItem, MemoryItem } from 'src/types';
|
||||
@ -13,7 +13,11 @@ export const newDate = () => new Date();
|
||||
export const newUpdateId = () => 'uuid-v7';
|
||||
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(),
|
||||
isAdmin: false,
|
||||
name: 'Test User',
|
||||
@ -23,7 +27,7 @@ const authUser = (authUser: Partial<AuthUser>) => ({
|
||||
...authUser,
|
||||
});
|
||||
|
||||
const user = (user: Partial<User>) => ({
|
||||
const userFactory = (user: Partial<User> = {}) => ({
|
||||
id: newUuid(),
|
||||
name: 'Test User',
|
||||
email: 'test@immich.cloud',
|
||||
@ -32,75 +36,95 @@ const user = (user: Partial<User>) => ({
|
||||
...user,
|
||||
});
|
||||
|
||||
export const factory = {
|
||||
auth: (user: Partial<AuthUser> = {}) => ({
|
||||
user: authUser(user),
|
||||
}),
|
||||
authUser,
|
||||
user,
|
||||
asset: (asset: Partial<Asset> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
deletedAt: null,
|
||||
updateId: newUpdateId(),
|
||||
status: AssetStatus.ACTIVE,
|
||||
checksum: newSha1(),
|
||||
deviceAssetId: '',
|
||||
deviceId: '',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
encodedVideoPath: null,
|
||||
fileCreatedAt: newDate(),
|
||||
fileModifiedAt: newDate(),
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
isVisible: true,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: newDate(),
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
originalPath: `upload/12/34/IMG_123.jpg`,
|
||||
ownerId: newUuid(),
|
||||
sidecarPath: null,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetType.IMAGE,
|
||||
...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> = {}) => ({
|
||||
const assetFactory = (asset: Partial<Asset> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
deletedAt: null,
|
||||
updateId: newUpdateId(),
|
||||
status: AssetStatus.ACTIVE,
|
||||
checksum: newSha1(),
|
||||
deviceAssetId: '',
|
||||
deviceId: '',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
encodedVideoPath: null,
|
||||
fileCreatedAt: newDate(),
|
||||
fileModifiedAt: newDate(),
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
isVisible: true,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: newDate(),
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
originalPath: `upload/12/34/IMG_123.jpg`,
|
||||
ownerId: newUuid(),
|
||||
sidecarPath: null,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetType.IMAGE,
|
||||
...asset,
|
||||
});
|
||||
|
||||
const activityFactory = (activity: Partial<ActivityItem> = {}) => {
|
||||
const userId = activity.userId || newUuid();
|
||||
return {
|
||||
id: newUuid(),
|
||||
comment: null,
|
||||
isLiked: false,
|
||||
userId,
|
||||
user: userFactory({ id: userId }),
|
||||
assetId: newUuid(),
|
||||
albumId: 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,
|
||||
}),
|
||||
...activity,
|
||||
};
|
||||
};
|
||||
|
||||
const libraryFactory = (library: Partial<Library> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
updateId: newUpdateId(),
|
||||
deletedAt: null,
|
||||
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%"
|
||||
/>
|
||||
{#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" />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -50,9 +50,11 @@
|
||||
<button
|
||||
type="button"
|
||||
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}
|
||||
>
|
||||
<Icon path={mdiChevronLeft} size="36" /></button
|
||||
<Icon path={mdiChevronLeft} size="36" ariaLabel={$t('previous')} /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
@ -61,9 +63,11 @@
|
||||
<button
|
||||
type="button"
|
||||
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}
|
||||
>
|
||||
<Icon path={mdiChevronRight} size="36" /></button
|
||||
<Icon path={mdiChevronRight} size="36" ariaLabel={$t('next')} /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -32,6 +32,7 @@
|
||||
let showFilter = $state(false);
|
||||
let isSearchSuggestions = $state(false);
|
||||
let selectedId: string | undefined = $state();
|
||||
let isFocus = $state(false);
|
||||
|
||||
const listboxId = generateId();
|
||||
|
||||
@ -98,7 +99,25 @@
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@ -132,10 +151,12 @@
|
||||
|
||||
const openDropdown = () => {
|
||||
showSuggestions = true;
|
||||
isFocus = true;
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
showSuggestions = false;
|
||||
isFocus = false;
|
||||
searchHistoryBox?.clearSelection();
|
||||
};
|
||||
|
||||
@ -143,6 +164,26 @@
|
||||
event.preventDefault();
|
||||
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>
|
||||
|
||||
<svelte:window
|
||||
@ -214,6 +255,21 @@
|
||||
<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" />
|
||||
</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}
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<CircleIconButton onclick={onClear} icon={mdiClose} title={$t('clear')} size="20" />
|
||||
|
@ -2,7 +2,7 @@
|
||||
import type { SearchLocationFilter } from './search-location-section.svelte';
|
||||
import type { SearchDisplayFilters } from './search-display-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 = {
|
||||
query: string;
|
||||
@ -55,9 +55,18 @@
|
||||
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({
|
||||
query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '',
|
||||
queryType: 'smart',
|
||||
queryType: defaultQueryType(),
|
||||
personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []),
|
||||
tagIds: new SvelteSet('tagIds' in searchQuery ? searchQuery.tagIds : []),
|
||||
location: {
|
||||
@ -90,7 +99,7 @@
|
||||
const resetForm = () => {
|
||||
filter = {
|
||||
query: '',
|
||||
queryType: 'smart',
|
||||
queryType: defaultQueryType(), // retain from localStorage or default
|
||||
personIds: new SvelteSet(),
|
||||
tagIds: new SvelteSet(),
|
||||
location: {},
|
||||
@ -142,8 +151,14 @@
|
||||
|
||||
const onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
storeQueryType(filter.queryType);
|
||||
search();
|
||||
};
|
||||
|
||||
// Will be called whenever queryType changes, not just onsubmit.
|
||||
$effect(() => {
|
||||
storeQueryType(filter.queryType);
|
||||
});
|
||||
</script>
|
||||
|
||||
<FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}>
|
||||
|
@ -119,6 +119,14 @@ export const fallbackLocale = {
|
||||
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 = [
|
||||
{ code: 'af-ZA', name: 'Afrikaans (South Africa)' },
|
||||
{ code: 'sq-AL', name: 'Albanian (Albania)' },
|
||||
|
Loading…
x
Reference in New Issue
Block a user