1
0
mirror of https://github.com/immich-app/immich.git synced 2025-07-03 05:46:58 +02:00

Merge remote-tracking branch 'origin/main' into fix-search-scrollbar

This commit is contained in:
Snowknight26
2025-03-09 23:22:27 -05:00
85 changed files with 3599 additions and 4529 deletions

View File

@ -41,8 +41,8 @@ jobs:
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
- name: Install Poetry - name: Install uv
run: pipx install poetry uses: astral-sh/setup-uv@v5
- name: Bump version - name: Bump version
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"

View File

@ -380,27 +380,28 @@ jobs:
working-directory: ./machine-learning working-directory: ./machine-learning
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install poetry - name: Install uv
run: pipx install poetry uses: astral-sh/setup-uv@v5
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
python-version: 3.11 # with:
cache: 'poetry' # python-version: 3.11
# cache: 'uv'
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install --with dev --with cpu uv sync --extra cpu
- name: Lint with ruff - name: Lint with ruff
run: | run: |
poetry run ruff check --output-format=github app export uv run ruff check --output-format=github app export
- name: Check black formatting - name: Check black formatting
run: | run: |
poetry run black --check app export uv run black --check app export
- name: Run mypy type checking - name: Run mypy type checking
run: | run: |
poetry run mypy --install-types --non-interactive --strict app/ uv run mypy --strict app/
- name: Run tests and coverage - name: Run tests and coverage
run: | run: |
poetry run pytest app --cov=app --cov-report term-missing uv run pytest app --cov=app --cov-report term-missing
shellcheck: shellcheck:
name: ShellCheck name: ShellCheck

View File

@ -25,7 +25,7 @@ services:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
target: dev target: dev
restart: always restart: unless-stopped
volumes: volumes:
- ../server:/usr/src/app - ../server:/usr/src/app
- ../open-api:/usr/src/open-api - ../open-api:/usr/src/open-api

View File

@ -53,7 +53,7 @@ docker compose create # Create Docker containers for Immich apps witho
docker start immich_postgres # Start Postgres server docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up sleep 10 # Wait for Postgres server to start up
# Check the database user if you deviated from the default # Check the database user if you deviated from the default
gunzip < "/path/to/backup/dump.sql.gz" \ gunzip --stdout "/path/to/backup/dump.sql.gz" \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \ | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup | docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
docker compose up -d # Start remainder of Immich apps docker compose up -d # Start remainder of Immich apps
@ -76,8 +76,8 @@ docker compose create # Create Docker containers for
docker start immich_postgres # Start Postgres server docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up sleep 10 # Wait for Postgres server to start up
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip` # Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip --stdout`
cat < "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME> cat "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
exit # Exit the Docker shell exit # Exit the Docker shell
docker compose up -d # Start remainder of Immich apps docker compose up -d # Start remainder of Immich apps
``` ```

View File

@ -201,7 +201,7 @@ describe('/people', () => {
expect(body).toMatchObject({ expect(body).toMatchObject({
id: expect.any(String), id: expect.any(String),
name: 'New Person', name: 'New Person',
birthDate: '1990-01-01T00:00:00.000Z', birthDate: '1990-01-01',
}); });
}); });
@ -262,7 +262,7 @@ describe('/people', () => {
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: '1990-01-01' }); .send({ birthDate: '1990-01-01' });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: '1990-01-01T00:00:00.000Z' }); expect(body).toMatchObject({ birthDate: '1990-01-01' });
}); });
it('should clear a date of birth', async () => { it('should clear a date of birth', async () => {

View File

@ -19,20 +19,16 @@ FROM builder-${DEVICE} AS builder
ARG DEVICE ARG DEVICE
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1
PIP_NO_CACHE_DIR=true \ WORKDIR /usr/src/app
VIRTUAL_ENV="/opt/venv" \
PATH="/opt/venv/bin:${PATH}"
RUN apt-get update && apt-get install -y --no-install-recommends g++ RUN apt-get update && apt-get install -y --no-install-recommends g++
RUN pip install --upgrade pip && pip install poetry COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN poetry config installer.max-workers 10 && \ RUN --mount=type=cache,target=/root/.cache/uv \
poetry config virtualenvs.create false --mount=type=bind,source=uv.lock,target=uv.lock \
RUN python3 -m venv /opt/venv --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu FROM python:3.11-slim-bookworm@sha256:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu
@ -93,7 +89,7 @@ WORKDIR /usr/src/app
ENV TRANSFORMERS_CACHE=/cache \ ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \ PATH="/usr/src/app/.venv/bin:$PATH" \
PYTHONPATH=/usr/src \ PYTHONPATH=/usr/src \
DEVICE=${DEVICE} DEVICE=${DEVICE}
@ -102,7 +98,7 @@ RUN echo "hard core 0" >> /etc/security/limits.conf && \
echo "fs.suid_dumpable 0" >> /etc/sysctl.conf && \ echo "fs.suid_dumpable 0" >> /etc/sysctl.conf && \
echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile
COPY --from=builder /opt/venv /opt/venv COPY --from=builder /usr/src/app/.venv /usr/src/app/.venv
COPY ann/ann.py /usr/src/ann/ann.py COPY ann/ann.py /usr/src/ann/ann.py
COPY start.sh log_conf.json gunicorn_conf.py ./ COPY start.sh log_conf.json gunicorn_conf.py ./
COPY app . COPY app .

View File

@ -5,13 +5,12 @@
# Setup # Setup
This project uses [Poetry](https://python-poetry.org/docs/#installation), so be sure to install it first. This project uses [uv](https://docs.astral.sh/uv/getting-started/installation/), so be sure to install it first.
Running `poetry install --no-root --with dev --with cpu` will install everything you need in an isolated virtual environment. Running `uv sync --extra cpu` will install everything you need in an isolated virtual environment.
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required. CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--group cpu` with either of `--group cuda` or `--group openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
To add or remove dependencies, you can use the commands `poetry add $PACKAGE_NAME` and `poetry remove $PACKAGE_NAME`, respectively.
Be sure to commit the `poetry.lock` and `pyproject.toml` files with `poetry lock --no-update` to reflect any changes in dependencies.
To add or remove dependencies, you can use the commands `uv add $PACKAGE_NAME` and `uv remove $PACKAGE_NAME`, respectively.
Be sure to commit the `uv.lock` and `pyproject.toml` files with `uv lock` to reflect any changes in dependencies.
# Load Testing # Load Testing
@ -26,15 +25,18 @@ Note that in Locust's jargon, concurrency is measured in `users`, and each user
# Facial Recognition # Facial Recognition
## Acknowledgements ## Acknowledgements
This project utilizes facial recognition models from the [InsightFace](https://github.com/deepinsight/insightface/tree/master/model_zoo) project. We appreciate the work put into developing these models, which have been beneficial to the machine learning part of this project. This project utilizes facial recognition models from the [InsightFace](https://github.com/deepinsight/insightface/tree/master/model_zoo) project. We appreciate the work put into developing these models, which have been beneficial to the machine learning part of this project.
### Used Models ### Used Models
* antelopev2
* buffalo_l - antelopev2
* buffalo_m - buffalo_l
* buffalo_s - buffalo_m
- buffalo_s
## License and Use Restrictions ## License and Use Restrictions
We have received permission to use the InsightFace facial recognition models in our project, as granted via email by Jia Guo (guojia@insightface.ai) on 18th March 2023. However, it's important to note that this permission does not extend to the redistribution or commercial use of their models by third parties. Users and developers interested in using these models should review the licensing terms provided in the InsightFace GitHub repository. We have received permission to use the InsightFace facial recognition models in our project, as granted via email by Jia Guo (guojia@insightface.ai) on 18th March 2023. However, it's important to note that this permission does not extend to the redistribution or commercial use of their models by third parties. Users and developers interested in using these models should review the licensing terms provided in the InsightFace GitHub repository.
For more information on the capabilities of the InsightFace models and to ensure compliance with their license, please refer to their [official repository](https://github.com/deepinsight/insightface). Adhering to the specified licensing terms is crucial for the respectful and lawful use of their work. For more information on the capabilities of the InsightFace models and to ensure compliance with their license, please refer to their [official repository](https://github.com/deepinsight/insightface). Adhering to the specified licensing terms is crucial for the respectful and lawful use of their work.

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +1,77 @@
[tool.poetry] [project]
name = "machine-learning" name = "machine-learning"
version = "1.129.0" version = "1.129.0"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.10,<4.0"
readme = "README.md" readme = "README.md"
packages = [{include = "app"}] dependencies = [
"aiocache>=0.12.1,<1.0",
"fastapi>=0.95.2,<1.0",
"ftfy>=6.1.1",
"gunicorn>=21.1.0",
"huggingface-hub>=0.20.1,<1.0",
"insightface>=0.7.3,<1.0",
"opencv-python-headless>=4.7.0.72,<5.0",
"orjson>=3.9.5",
"pillow>=9.5.0,<11.0",
"pydantic>=2.0.0,<3",
"pydantic-settings>=2.5.2,<3",
"python-multipart>=0.0.6,<1.0",
"rich>=13.4.2",
"tokenizers>=0.15.0,<1.0",
"uvicorn[standard]>=0.22.0,<1.0",
]
[tool.poetry.dependencies] [dependency-groups]
python = ">=3.10,<4.0" test = [
insightface = ">=0.7.3,<1.0" "httpx>=0.24.1",
opencv-python-headless = ">=4.7.0.72,<5.0" "pytest>=7.3.1",
pillow = ">=9.5.0,<11.0" "pytest-asyncio>=0.21.0",
fastapi = ">=0.95.2,<1.0" "pytest-cov>=4.1.0",
uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"} "pytest-mock>=3.11.1",
pydantic = "^2.0.0" ]
pydantic-settings = "^2.5.2" types = [
aiocache = ">=0.12.1,<1.0" "types-pyyaml>=6.0.12.20241230",
rich = ">=13.4.2" "types-requests>=2.32.0.20250306",
ftfy = ">=6.1.1" "types-setuptools>=75.8.2.20250305",
python-multipart = ">=0.0.6,<1.0" "types-simplejson>=3.20.0.20250218",
orjson = ">=3.9.5" "types-ujson>=5.10.0.20240515",
gunicorn = ">=21.1.0" ]
huggingface-hub = ">=0.20.1,<1.0" lint = [
tokenizers = ">=0.15.0,<1.0" "black>=23.3.0",
"mypy>=1.3.0",
"ruff>=0.0.272",
{ include-group = "types" },
]
dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }]
[tool.poetry.group.dev.dependencies] [project.optional-dependencies]
mypy = ">=1.3.0" cpu = ["onnxruntime>=1.15.0,<2"]
black = ">=23.3.0" cuda = ["onnxruntime-gpu>=1.17.0,<2"]
pytest = ">=7.3.1" openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0"]
locust = ">=2.15.1" armnn = ["onnxruntime>=1.15.0,<2"]
httpx = ">=0.24.1"
pytest-asyncio = ">=0.21.0"
pytest-cov = ">=4.1.0"
ruff = ">=0.0.272"
pytest-mock = ">=3.11.1"
[tool.poetry.group.cpu] [tool.uv]
optional = true compile-bytecode = true
[tool.poetry.group.cpu.dependencies] [[tool.uv.index]]
onnxruntime = "^1.15.0"
[tool.poetry.group.cuda]
optional = true
[tool.poetry.group.cuda.dependencies]
onnxruntime-gpu = {version = "^1.17.0", source = "cuda12"}
[tool.poetry.group.openvino]
optional = true
[tool.poetry.group.openvino.dependencies]
onnxruntime-openvino = ">=1.17.1,<1.19.0"
[tool.poetry.group.armnn]
optional = true
[tool.poetry.group.armnn.dependencies]
onnxruntime = "^1.15.0"
[[tool.poetry.source]]
name = "cuda12" name = "cuda12"
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
priority = "explicit" explicit = true
[tool.uv.sources]
onnxruntime-gpu = { index = "cuda12" }
[tool.hatch.build.targets.sdist]
include = ["app"]
[tool.hatch.build.targets.wheel]
include = ["app"]
[build-system] [build-system]
requires = ["poetry-core"] requires = ["hatchling"]
build-backend = "poetry.core.masonry.api" build-backend = "hatchling.build"
[tool.mypy] [tool.mypy]
python_version = "3.11" python_version = "3.11"

2648
machine-learning/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
npm --prefix web i --package-lock-only npm --prefix web i --package-lock-only
npm --prefix e2e version "$SERVER_PUMP" npm --prefix e2e version "$SERVER_PUMP"
npm --prefix e2e i --package-lock-only npm --prefix e2e i --package-lock-only
poetry --directory machine-learning version "$SERVER_PUMP" uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "$SERVER_PUMP"
fi fi
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then

View File

@ -93,9 +93,9 @@
<activity <activity
android:name="com.linusu.flutter_web_auth.CallbackActivity" android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true"> android:exported="true">
<intent-filter android:label="flutter_web_auth"> <intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />

View File

@ -264,6 +264,7 @@
"exif_bottom_sheet_location_add": "Add a location", "exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE", "exif_bottom_sheet_people": "PEOPLE",
"exif_bottom_sheet_person_add_person": "Add name", "exif_bottom_sheet_person_add_person": "Add name",
"exif_bottom_sheet_person_age": "Age {}",
"experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_subtitle": "Use at your own risk!",

View File

@ -48,7 +48,7 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- Flutter - Flutter
- SAMKeychain - SAMKeychain
- flutter_web_auth (0.6.0): - flutter_web_auth_2 (3.0.0):
- Flutter - Flutter
- fluttertoast (0.0.2): - fluttertoast (0.0.2):
- Flutter - Flutter
@ -117,7 +117,7 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`) - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
@ -166,8 +166,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_udid: flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios" :path: ".symlinks/plugins/flutter_udid/ios"
flutter_web_auth: flutter_web_auth_2:
:path: ".symlinks/plugins/flutter_web_auth/ios" :path: ".symlinks/plugins/flutter_web_auth_2/ios"
fluttertoast: fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
geolocator_apple: geolocator_apple:
@ -220,7 +220,7 @@ SPEC CHECKSUMS:
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
flutter_web_auth: acc15a8fd7bba796a933c724a6dffc3d00f07c27 flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450 geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1

View File

@ -8,20 +8,26 @@ class SearchCuratedContent {
/// The label to show associated with this curated object /// The label to show associated with this curated object
final String label; final String label;
/// The subtitle to show below the label
final String? subtitle;
/// The id to lookup the asset from the server /// The id to lookup the asset from the server
final String id; final String id;
SearchCuratedContent({ SearchCuratedContent({
required this.label, required this.label,
required this.id, required this.id,
this.subtitle,
}); });
SearchCuratedContent copyWith({ SearchCuratedContent copyWith({
String? label, String? label,
String? subtitle,
String? id, String? id,
}) { }) {
return SearchCuratedContent( return SearchCuratedContent(
label: label ?? this.label, label: label ?? this.label,
subtitle: subtitle ?? this.subtitle,
id: id ?? this.id, id: id ?? this.id,
); );
} }
@ -29,6 +35,7 @@ class SearchCuratedContent {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return <String, dynamic>{ return <String, dynamic>{
'label': label, 'label': label,
'subtitle': subtitle,
'id': id, 'id': id,
}; };
} }
@ -36,6 +43,7 @@ class SearchCuratedContent {
factory SearchCuratedContent.fromMap(Map<String, dynamic> map) { factory SearchCuratedContent.fromMap(Map<String, dynamic> map) {
return SearchCuratedContent( return SearchCuratedContent(
label: map['label'] as String, label: map['label'] as String,
subtitle: map['subtitle'] as String?,
id: map['id'] as String, id: map['id'] as String,
); );
} }
@ -46,13 +54,14 @@ class SearchCuratedContent {
SearchCuratedContent.fromMap(json.decode(source) as Map<String, dynamic>); SearchCuratedContent.fromMap(json.decode(source) as Map<String, dynamic>);
@override @override
String toString() => 'CuratedContent(label: $label, id: $id)'; String toString() =>
'CuratedContent(label: $label, subtitle: $subtitle, id: $id)';
@override @override
bool operator ==(covariant SearchCuratedContent other) { bool operator ==(covariant SearchCuratedContent other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.label == label && other.id == id; return other.label == label && other.subtitle == subtitle && other.id == id;
} }
@override @override

View File

@ -187,6 +187,8 @@ class AlbumActivityProvider extends AutoDisposeAsyncNotifierProviderImpl<
} }
} }
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef<List<Activity>> { mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef<List<Activity>> {
/// The parameter `albumId` of this provider. /// The parameter `albumId` of this provider.
String get albumId; String get albumId;
@ -206,4 +208,4 @@ class _AlbumActivityProviderElement
String? get assetId => (origin as AlbumActivityProvider).assetId; String? get assetId => (origin as AlbumActivityProvider).assetId;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart';
import 'package:immich_mobile/services/activity.service.dart'; import 'package:immich_mobile/services/activity.service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -5,5 +6,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity_service.provider.g.dart'; part 'activity_service.provider.g.dart';
@riverpod @riverpod
ActivityService activityService(ActivityServiceRef ref) => ActivityService activityService(Ref ref) =>
ActivityService(ref.watch(activityApiRepositoryProvider)); ActivityService(ref.watch(activityApiRepositoryProvider));

View File

@ -20,6 +20,8 @@ final activityServiceProvider = AutoDisposeProvider<ActivityService>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>; typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -186,6 +186,8 @@ class ActivityStatisticsProvider
} }
} }
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef<int> { mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef<int> {
/// The parameter `albumId` of this provider. /// The parameter `albumId` of this provider.
String get albumId; String get albumId;
@ -205,4 +207,4 @@ class _ActivityStatisticsProviderElement
String? get assetId => (origin as ActivityStatisticsProvider).assetId; String? get assetId => (origin as ActivityStatisticsProvider).assetId;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -40,4 +40,4 @@ final albumSortOrderProvider =
typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>; typedef _$AlbumSortOrder = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -22,4 +22,4 @@ final currentAlbumProvider =
typedef _$CurrentAlbum = AutoDisposeNotifier<Album?>; typedef _$CurrentAlbum = AutoDisposeNotifier<Album?>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,7 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'api.provider.g.dart'; part 'api.provider.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
ApiService apiService(ApiServiceRef ref) => ApiService(); ApiService apiService(Ref ref) => ApiService();

View File

@ -19,6 +19,8 @@ final apiServiceProvider = Provider<ApiService>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ApiServiceRef = ProviderRef<ApiService>; typedef ApiServiceRef = ProviderRef<ApiService>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,8 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_settings.provider.g.dart'; part 'app_settings.provider.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
AppSettingsService appSettingsService(AppSettingsServiceRef ref) => AppSettingsService appSettingsService(Ref ref) => AppSettingsService();
AppSettingsService();

View File

@ -21,6 +21,8 @@ final appSettingsServiceProvider = Provider<AppSettingsService>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AppSettingsServiceRef = ProviderRef<AppSettingsService>; typedef AppSettingsServiceRef = ProviderRef<AppSettingsService>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -31,7 +31,7 @@ class AssetNotifier extends StateNotifier<bool> {
final SyncService _syncService; final SyncService _syncService;
final ETagService _etagService; final ETagService _etagService;
final ExifService _exifService; final ExifService _exifService;
final StateNotifierProviderRef _ref; final Ref _ref;
final log = Logger('AssetNotifier'); final log = Logger('AssetNotifier');
bool _getAllAssetInProgress = false; bool _getAllAssetInProgress = false;
bool _deleteInProgress = false; bool _deleteInProgress = false;

View File

@ -171,6 +171,8 @@ class AssetPeopleNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl<
} }
} }
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AssetPeopleNotifierRef mixin AssetPeopleNotifierRef
on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> { on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> {
/// The parameter `asset` of this provider. /// The parameter `asset` of this provider.
@ -186,4 +188,4 @@ class _AssetPeopleNotifierProviderElement
Asset get asset => (origin as AssetPeopleNotifierProvider).asset; Asset get asset => (origin as AssetPeopleNotifierProvider).asset;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -39,6 +39,6 @@ final assetStackStateProvider = StateNotifierProvider.autoDispose
); );
@riverpod @riverpod
int assetStackIndex(AssetStackIndexRef ref, Asset asset) { int assetStackIndex(Ref ref, Asset asset) {
return -1; return -1;
} }

View File

@ -142,6 +142,8 @@ class AssetStackIndexProvider extends AutoDisposeProvider<int> {
} }
} }
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AssetStackIndexRef on AutoDisposeProviderRef<int> { mixin AssetStackIndexRef on AutoDisposeProviderRef<int> {
/// The parameter `asset` of this provider. /// The parameter `asset` of this provider.
Asset get asset; Asset get asset;
@ -155,4 +157,4 @@ class _AssetStackIndexProviderElement extends AutoDisposeProviderElement<int>
Asset get asset => (origin as AssetStackIndexProvider).asset; Asset get asset => (origin as AssetStackIndexProvider).asset;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -22,4 +22,4 @@ final currentAssetProvider =
typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>; typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -24,4 +24,4 @@ final backupVerificationProvider =
typedef _$BackupVerification = AutoDisposeNotifier<bool>; typedef _$BackupVerification = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,12 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'immich_logo_provider.g.dart'; part 'immich_logo_provider.g.dart';
@riverpod @riverpod
Future<Uint8List> immichLogo(ImmichLogoRef ref) async { Future<Uint8List> immichLogo(Ref ref) async {
final json = await rootBundle.loadString('assets/immich-logo.json'); final json = await rootBundle.loadString('assets/immich-logo.json');
final j = jsonDecode(json); final j = jsonDecode(json);
return base64Decode(j['content']); return base64Decode(j['content']);

View File

@ -19,6 +19,8 @@ final immichLogoProvider = AutoDisposeFutureProvider<Uint8List>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ImmichLogoRef = AutoDisposeFutureProviderRef<Uint8List>; typedef ImmichLogoRef = AutoDisposeFutureProviderRef<Uint8List>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,7 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'db.provider.g.dart'; part 'db.provider.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
Isar isar(IsarRef ref) => throw UnimplementedError('isar'); Isar isar(Ref ref) => throw UnimplementedError('isar');

View File

@ -19,6 +19,8 @@ final isarProvider = Provider<Isar>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef IsarRef = ProviderRef<Isar>; typedef IsarRef = ProviderRef<Isar>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@ -6,5 +7,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'exif.provider.g.dart'; part 'exif.provider.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
IExifInfoRepository exifRepository(ExifRepositoryRef ref) => IExifInfoRepository exifRepository(Ref ref) =>
IsarExifRepository(ref.watch(isarProvider)); IsarExifRepository(ref.watch(isarProvider));

View File

@ -20,6 +20,8 @@ final exifRepositoryProvider = Provider<IExifInfoRepository>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ExifRepositoryRef = ProviderRef<IExifInfoRepository>; typedef ExifRepositoryRef = ProviderRef<IExifInfoRepository>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@ -6,5 +7,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'store.provider.g.dart'; part 'store.provider.g.dart';
@riverpod @riverpod
IStoreRepository storeRepository(StoreRepositoryRef ref) => IStoreRepository storeRepository(Ref ref) =>
IsarStoreRepository(ref.watch(isarProvider)); IsarStoreRepository(ref.watch(isarProvider));

View File

@ -20,6 +20,8 @@ final storeRepositoryProvider = AutoDisposeProvider<IStoreRepository>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>; typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/providers/map/map_service.provider.dart'; import 'package:immich_mobile/providers/map/map_service.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart';
@ -6,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'map_marker.provider.g.dart'; part 'map_marker.provider.g.dart';
@riverpod @riverpod
Future<List<MapMarker>> mapMarkers(MapMarkersRef ref) async { Future<List<MapMarker>> mapMarkers(Ref ref) async {
final service = ref.read(mapServiceProvider); final service = ref.read(mapServiceProvider);
final mapState = ref.read(mapStateNotifierProvider); final mapState = ref.read(mapStateNotifierProvider);
DateTime? fileCreatedAfter; DateTime? fileCreatedAfter;

View File

@ -19,6 +19,8 @@ final mapMarkersProvider = AutoDisposeFutureProvider<List<MapMarker>>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef MapMarkersRef = AutoDisposeFutureProviderRef<List<MapMarker>>; typedef MapMarkersRef = AutoDisposeFutureProviderRef<List<MapMarker>>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/map.service.dart'; import 'package:immich_mobile/services/map.service.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -5,5 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'map_service.provider.g.dart'; part 'map_service.provider.g.dart';
@riverpod @riverpod
MapSerivce mapService(MapServiceRef ref) => MapSerivce mapService(Ref ref) => MapSerivce(ref.watch(apiServiceProvider));
MapSerivce(ref.watch(apiServiceProvider));

View File

@ -19,6 +19,8 @@ final mapServiceProvider = AutoDisposeProvider<MapSerivce>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef MapServiceRef = AutoDisposeProviderRef<MapSerivce>; typedef MapServiceRef = AutoDisposeProviderRef<MapSerivce>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -23,4 +23,4 @@ final mapStateNotifierProvider =
typedef _$MapStateNotifier = Notifier<MapState>; typedef _$MapStateNotifier = Notifier<MapState>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -45,7 +45,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
@riverpod @riverpod
Future<RenderList> paginatedSearchRenderList( Future<RenderList> paginatedSearchRenderList(
PaginatedSearchRenderListRef ref, Ref ref,
) { ) {
final result = ref.watch(paginatedSearchProvider); final result = ref.watch(paginatedSearchProvider);
final timelineService = ref.watch(timelineServiceProvider); final timelineService = ref.watch(timelineServiceProvider);

View File

@ -22,6 +22,8 @@ final paginatedSearchRenderListProvider =
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef<RenderList>; typedef PaginatedSearchRenderListRef = AutoDisposeFutureProviderRef<RenderList>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/services/person.service.dart'; import 'package:immich_mobile/services/person.service.dart';
@ -9,7 +10,7 @@ part 'people.provider.g.dart';
@riverpod @riverpod
Future<List<Person>> getAllPeople( Future<List<Person>> getAllPeople(
GetAllPeopleRef ref, Ref ref,
) async { ) async {
final PersonService personService = ref.read(personServiceProvider); final PersonService personService = ref.read(personServiceProvider);
@ -19,7 +20,7 @@ Future<List<Person>> getAllPeople(
} }
@riverpod @riverpod
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async { Future<RenderList> personAssets(Ref ref, String personId) async {
final PersonService personService = ref.read(personServiceProvider); final PersonService personService = ref.read(personServiceProvider);
final assets = await personService.getPersonAssets(personId); final assets = await personService.getPersonAssets(personId);
@ -31,7 +32,7 @@ Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
@riverpod @riverpod
Future<bool> updatePersonName( Future<bool> updatePersonName(
UpdatePersonNameRef ref, Ref ref,
String personId, String personId,
String updatedName, String updatedName,
) async { ) async {

View File

@ -19,6 +19,8 @@ final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>; typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>;
String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832'; String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832';
@ -156,6 +158,8 @@ class PersonAssetsProvider extends AutoDisposeFutureProvider<RenderList> {
} }
} }
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PersonAssetsRef on AutoDisposeFutureProviderRef<RenderList> { mixin PersonAssetsRef on AutoDisposeFutureProviderRef<RenderList> {
/// The parameter `personId` of this provider. /// The parameter `personId` of this provider.
String get personId; String get personId;
@ -296,6 +300,8 @@ class UpdatePersonNameProvider extends AutoDisposeFutureProvider<bool> {
} }
} }
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef<bool> { mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef<bool> {
/// The parameter `personId` of this provider. /// The parameter `personId` of this provider.
String get personId; String get personId;
@ -314,4 +320,4 @@ class _UpdatePersonNameProviderElement
String get updatedName => (origin as UpdatePersonNameProvider).updatedName; String get updatedName => (origin as UpdatePersonNameProvider).updatedName;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/search.service.dart'; import 'package:immich_mobile/services/search.service.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -6,7 +7,7 @@ part 'search_filter.provider.g.dart';
@riverpod @riverpod
Future<List<String>> getSearchSuggestions( Future<List<String>> getSearchSuggestions(
GetSearchSuggestionsRef ref, Ref ref,
SearchSuggestionType type, { SearchSuggestionType type, {
String? locationCountry, String? locationCountry,
String? locationState, String? locationState,

View File

@ -189,6 +189,8 @@ class GetSearchSuggestionsProvider
} }
} }
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef<List<String>> { mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef<List<String>> {
/// The parameter `type` of this provider. /// The parameter `type` of this provider.
SearchSuggestionType get type; SearchSuggestionType get type;
@ -226,4 +228,4 @@ class _GetSearchSuggestionsProviderElement
String? get model => (origin as GetSearchSuggestionsProvider).model; String? get model => (origin as GetSearchSuggestionsProvider).model;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -788,6 +788,53 @@ class FilterImageRouteArgs {
} }
} }
/// generated route for
/// [FolderPage]
class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
FolderRoute({
Key? key,
RecursiveFolder? folder,
List<PageRouteInfo>? children,
}) : super(
FolderRoute.name,
args: FolderRouteArgs(
key: key,
folder: folder,
),
initialChildren: children,
);
static const String name = 'FolderRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args =
data.argsAs<FolderRouteArgs>(orElse: () => const FolderRouteArgs());
return FolderPage(
key: args.key,
folder: args.folder,
);
},
);
}
class FolderRouteArgs {
const FolderRouteArgs({
this.key,
this.folder,
});
final Key? key;
final RecursiveFolder? folder;
@override
String toString() {
return 'FolderRouteArgs{key: $key, folder: $folder}';
}
}
/// generated route for /// generated route for
/// [GalleryViewerPage] /// [GalleryViewerPage]
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> { class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
@ -1175,40 +1222,6 @@ class PartnerRoute extends PageRouteInfo<void> {
); );
} }
/// manually written (with love) route for
/// [FolderPage]
class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
FolderRoute({
RecursiveFolder? folder,
List<PageRouteInfo>? children,
}) : super(
FolderRoute.name,
args: FolderRouteArgs(folder: folder),
initialChildren: children,
);
static const String name = 'FolderRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<FolderRouteArgs>();
return FolderPage(folder: args.folder);
},
);
}
class FolderRouteArgs {
const FolderRouteArgs({this.folder});
final RecursiveFolder? folder;
@override
String toString() {
return 'FolderRouteArgs{folder: $folder}';
}
}
/// generated route for /// generated route for
/// [PeopleCollectionPage] /// [PeopleCollectionPage]
class PeopleCollectionRoute extends PageRouteInfo<void> { class PeopleCollectionRoute extends PageRouteInfo<void> {

View File

@ -1,7 +1,7 @@
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:flutter_web_auth/flutter_web_auth.dart';
// Redirect URL = app.immich:///oauth-callback // Redirect URL = app.immich:///oauth-callback
@ -32,7 +32,7 @@ class OAuthService {
} }
Future<LoginResponseDto?> oAuthLogin(String oauthUrl) async { Future<LoginResponseDto?> oAuthLogin(String oauthUrl) async {
String result = await FlutterWebAuth.authenticate( String result = await FlutterWebAuth2.authenticate(
url: oauthUrl, url: oauthUrl,
callbackUrlScheme: callbackUrlScheme, callbackUrlScheme: callbackUrlScheme,
); );

View File

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/interfaces/asset_api.interface.dart';
@ -11,7 +12,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'person.service.g.dart'; part 'person.service.g.dart';
@riverpod @riverpod
PersonService personService(PersonServiceRef ref) => PersonService( PersonService personService(Ref ref) => PersonService(
ref.watch(personApiRepositoryProvider), ref.watch(personApiRepositoryProvider),
ref.watch(assetApiRepositoryProvider), ref.watch(assetApiRepositoryProvider),
ref.read(assetRepositoryProvider), ref.read(assetRepositoryProvider),

View File

@ -20,6 +20,8 @@ final personServiceProvider = AutoDisposeProvider<PersonService>.internal(
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PersonServiceRef = AutoDisposeProviderRef<PersonService>; typedef PersonServiceRef = AutoDisposeProviderRef<PersonService>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -332,7 +332,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
); );
} }
if (index != -1 && index < widget.renderList.elements.length) { if (index < widget.renderList.elements.length) {
// Not sure why the index is shifted, but it works. :3 // Not sure why the index is shifted, but it works. :3
_scrollToIndex(index + 1); _scrollToIndex(index + 1);
} else { } else {

View File

@ -44,7 +44,19 @@ class PeopleInfo extends ConsumerWidget {
} }
final curatedPeople = people final curatedPeople = people
?.map((p) => SearchCuratedContent(id: p.id, label: p.name)) ?.map(
(p) => SearchCuratedContent(
id: p.id,
label: p.name,
subtitle: p.birthDate != null
? "exif_bottom_sheet_person_age".tr(
args: [
_calculateAge(p.birthDate!).toString(),
],
)
: null,
),
)
.toList() ?? .toList() ??
[]; [];
@ -99,4 +111,17 @@ class PeopleInfo extends ConsumerWidget {
), ),
); );
} }
int _calculateAge(DateTime birthDate) {
DateTime today = DateTime.now();
int age = today.year - birthDate.year;
// Check if the birthday has occurred this year
if (today.month < birthDate.month ||
(today.month == birthDate.month && today.day < birthDate.day)) {
age--;
}
return age;
}
} }

View File

@ -5,6 +5,8 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
@ -95,6 +97,16 @@ class GalleryAppBar extends ConsumerWidget {
ref.read(downloadStateProvider.notifier).downloadAsset(asset, context); ref.read(downloadStateProvider.notifier).downloadAsset(asset, context);
} }
handleLocateAsset() async {
// Go back to the gallery
await context.maybePop();
await context
.navigateTo(const TabControllerRoute(children: [PhotosRoute()]));
ref.read(tabProvider.notifier).update((state) => state = TabEnum.home);
// Scroll to the asset's date
scrollToDateNotifierProvider.scrollToDate(asset.fileCreatedAt);
}
return IgnorePointer( return IgnorePointer(
ignoring: !showControls, ignoring: !showControls,
child: AnimatedOpacity( child: AnimatedOpacity(
@ -107,6 +119,7 @@ class GalleryAppBar extends ConsumerWidget {
isPartner: isPartner, isPartner: isPartner,
asset: asset, asset: asset,
onMoreInfoPressed: showInfo, onMoreInfoPressed: showInfo,
onLocatePressed: handleLocateAsset,
onFavorite: toggleFavorite, onFavorite: toggleFavorite,
onRestorePressed: () => handleRestore(asset), onRestorePressed: () => handleRestore(asset),
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
class TopControlAppBar extends HookConsumerWidget { class TopControlAppBar extends HookConsumerWidget {
@ -13,6 +14,7 @@ class TopControlAppBar extends HookConsumerWidget {
required this.asset, required this.asset,
required this.onMoreInfoPressed, required this.onMoreInfoPressed,
required this.onDownloadPressed, required this.onDownloadPressed,
required this.onLocatePressed,
required this.onAddToAlbumPressed, required this.onAddToAlbumPressed,
required this.onRestorePressed, required this.onRestorePressed,
required this.onFavorite, required this.onFavorite,
@ -26,6 +28,7 @@ class TopControlAppBar extends HookConsumerWidget {
final Function onMoreInfoPressed; final Function onMoreInfoPressed;
final VoidCallback? onUploadPressed; final VoidCallback? onUploadPressed;
final VoidCallback? onDownloadPressed; final VoidCallback? onDownloadPressed;
final VoidCallback onLocatePressed;
final VoidCallback onAddToAlbumPressed; final VoidCallback onAddToAlbumPressed;
final VoidCallback onRestorePressed; final VoidCallback onRestorePressed;
final VoidCallback onActivitiesPressed; final VoidCallback onActivitiesPressed;
@ -54,6 +57,18 @@ class TopControlAppBar extends HookConsumerWidget {
); );
} }
Widget buildLocateButton() {
return IconButton(
onPressed: () {
onLocatePressed();
},
icon: Icon(
Icons.image_search,
color: Colors.grey[200],
),
);
}
Widget buildMoreInfoButton() { Widget buildMoreInfoButton() {
return IconButton( return IconButton(
onPressed: () { onPressed: () {
@ -159,6 +174,8 @@ class TopControlAppBar extends HookConsumerWidget {
shape: const Border(), shape: const Border(),
actions: [ actions: [
if (asset.isRemote && isOwner) buildFavoriteButton(a), if (asset.isRemote && isOwner) buildFavoriteButton(a),
if (isOwner && ref.read(tabProvider.notifier).state != TabEnum.home)
buildLocateButton(),
if (asset.livePhotoVideoId != null) const MotionPhotoButton(), if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),

View File

@ -86,12 +86,22 @@ class CuratedPeopleRow extends StatelessWidget {
).tr(), ).tr(),
); );
} }
return Text( return Column(
person.label, mainAxisSize: MainAxisSize.min,
textAlign: TextAlign.center, children: [
overflow: TextOverflow.ellipsis, Text(
style: context.textTheme.labelLarge, person.label,
maxLines: 2, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: context.textTheme.labelLarge,
maxLines: 2,
),
if (person.subtitle != null)
Text(
person.subtitle!,
textAlign: TextAlign.center,
),
],
); );
} }
} }

View File

@ -358,6 +358,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.10"
desktop_webview_window:
dependency: transitive
description:
name: desktop_webview_window
sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0"
url: "https://pub.dev"
source: hosted
version: "0.2.3"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -508,10 +516,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_hooks name: flutter_hooks
sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.20.5" version: "0.21.2"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -577,10 +585,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_riverpod name: flutter_riverpod
sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.1" version: "2.6.1"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -602,14 +610,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
flutter_web_auth: flutter_web_auth_2:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_web_auth name: flutter_web_auth_2
sha256: "95e4856e24fb6ac1678f5ff334743b63f782d839ab324543d29ccbd295176209" sha256: "561c32d32ed537853de43852c35849cf1d37f3482f41f22b718ab6112f96b333"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "5.0.0-alpha.0"
flutter_web_auth_2_platform_interface:
dependency: transitive
description:
name: flutter_web_auth_2_platform_interface
sha256: "45927587ebb2364cd273675ec95f6f67b81725754b416cef2b65cdc63fd3e853"
url: "https://pub.dev"
source: hosted
version: "5.0.0-alpha.0"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -712,10 +728,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: hooks_riverpod name: hooks_riverpod
sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.1" version: "2.6.1"
hotreloader: hotreloader:
dependency: transitive dependency: transitive
description: description:
@ -1276,42 +1292,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: riverpod name: riverpod
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.1" version: "2.6.1"
riverpod_analyzer_utils: riverpod_analyzer_utils:
dependency: transitive dependency: transitive
description: description:
name: riverpod_analyzer_utils name: riverpod_analyzer_utils
sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.3" version: "0.5.6"
riverpod_annotation: riverpod_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
name: riverpod_annotation name: riverpod_annotation
sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.6.1"
riverpod_generator: riverpod_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: riverpod_generator name: riverpod_generator
sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f" sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.6.1"
riverpod_lint: riverpod_lint:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: riverpod_lint name: riverpod_lint
sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7 sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.12" version: "2.6.1"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -1813,6 +1829,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.4" version: "1.1.4"
window_to_front:
dependency: transitive
description:
name: window_to_front
sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -1846,5 +1870,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.5.3 <4.0.0" dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.5" flutter: ">=3.24.5"

View File

@ -17,9 +17,9 @@ dependencies:
path_provider_ios: path_provider_ios:
photo_manager: ^3.6.1 photo_manager: ^3.6.1
photo_manager_image_provider: ^2.2.0 photo_manager_image_provider: ^2.2.0
flutter_hooks: ^0.20.4 flutter_hooks: ^0.21.2
hooks_riverpod: ^2.4.9 hooks_riverpod: ^2.6.1
riverpod_annotation: ^2.3.3 riverpod_annotation: ^2.6.1
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
flutter_cache_manager: ^3.3.1 flutter_cache_manager: ^3.3.1
intl: ^0.19.0 intl: ^0.19.0
@ -42,7 +42,7 @@ dependencies:
path_provider: ^2.1.2 path_provider: ^2.1.2
collection: ^1.18.0 collection: ^1.18.0
http_parser: ^4.0.2 http_parser: ^4.0.2
flutter_web_auth: 0.6.0 flutter_web_auth_2: ^5.0.0-alpha.0
easy_image_viewer: ^1.4.0 easy_image_viewer: ^1.4.0
isar: isar:
version: *isar_version version: *isar_version
@ -108,8 +108,8 @@ dev_dependencies:
integration_test: integration_test:
sdk: flutter sdk: flutter
custom_lint: ^0.6.4 custom_lint: ^0.6.4
riverpod_lint: ^2.3.7 riverpod_lint: ^2.6.1
riverpod_generator: ^2.3.9 riverpod_generator: ^2.6.1
mocktail: ^1.0.3 mocktail: ^1.0.3
immich_mobile_immich_lint: immich_mobile_immich_lint:
path: './immich_lint' path: './immich_lint'

View File

@ -32,7 +32,8 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"cookie-parser": "^1.4.6", "cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"exiftool-vendored": "^28.3.1", "exiftool-vendored": "^28.3.1",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
@ -81,8 +82,7 @@
"@types/archiver": "^6.0.0", "@types/archiver": "^6.0.0",
"@types/async-lock": "^1.4.2", "@types/async-lock": "^1.4.2",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/cookie": "^0.6.0", "@types/cookie-parser": "^1.4.8",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21", "@types/fluent-ffmpeg": "^2.1.21",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
@ -5612,13 +5612,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/cookie-parser": { "node_modules/@types/cookie-parser": {
"version": "1.4.8", "version": "1.4.8",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
@ -8071,12 +8064,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">=18"
} }
}, },
"node_modules/cookie-parser": { "node_modules/cookie-parser": {
@ -8092,6 +8085,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -8728,6 +8730,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": { "node_modules/engine.io/node_modules/debug": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",

View File

@ -58,7 +58,8 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"cookie-parser": "^1.4.6", "cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"exiftool-vendored": "^28.3.1", "exiftool-vendored": "^28.3.1",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
@ -107,8 +108,7 @@
"@types/archiver": "^6.0.0", "@types/archiver": "^6.0.0",
"@types/async-lock": "^1.4.2", "@types/async-lock": "^1.4.2",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/cookie": "^0.6.0", "@types/cookie-parser": "^1.4.8",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21", "@types/fluent-ffmpeg": "^2.1.21",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View File

@ -10,6 +10,20 @@ export type AuthUser = {
quotaSizeInBytes: number | null; quotaSizeInBytes: number | null;
}; };
export type Library = {
id: string;
ownerId: string;
createdAt: Date;
updatedAt: Date;
updateId: string;
name: string;
importPaths: string[];
exclusionPatterns: string[];
deletedAt: Date | null;
refreshedAt: Date | null;
assets?: Asset[];
};
export type AuthApiKey = { export type AuthApiKey = {
id: string; id: string;
permissions: Permission[]; permissions: Permission[];

View File

@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator'; import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
import { LibraryEntity } from 'src/entities/library.entity'; import { Library } from 'src/database';
import { Optional, ValidateUUID } from 'src/validation'; import { Optional, ValidateUUID } from 'src/validation';
export class CreateLibraryDto { export class CreateLibraryDto {
@ -120,7 +120,7 @@ export class LibraryStatsResponseDto {
usage = 0; usage = 0;
} }
export function mapLibrary(entity: LibraryEntity): LibraryResponseDto { export function mapLibrary(entity: Library): LibraryResponseDto {
let assetCount = 0; let assetCount = 0;
if (entity.assets) { if (entity.assets) {
assetCount = entity.assets.length; assetCount = entity.assets.length;

View File

@ -7,6 +7,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { SourceType } from 'src/enum'; import { SourceType } from 'src/enum';
import { asDateString } from 'src/utils/date';
import { import {
IsDateStringFormat, IsDateStringFormat,
MaxDateString, MaxDateString,
@ -32,7 +33,7 @@ export class PersonCreateDto {
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' }) @MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
@IsDateStringFormat('yyyy-MM-dd') @IsDateStringFormat('yyyy-MM-dd')
@Optional({ nullable: true }) @Optional({ nullable: true })
birthDate?: string | null; birthDate?: Date | null;
/** /**
* Person visibility * Person visibility
@ -222,7 +223,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
return { return {
id: person.id, id: person.id,
name: person.name, name: person.name,
birthDate: person.birthDate, birthDate: asDateString(person.birthDate),
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden, isHidden: person.isHidden,
isFavorite: person.isFavorite, isFavorite: person.isFavorite,

View File

@ -26,7 +26,7 @@ export class LibraryEntity {
assets!: AssetEntity[]; assets!: AssetEntity[];
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
owner!: UserEntity; owner?: UserEntity;
@Column() @Column()
ownerId!: string; ownerId!: string;

View File

@ -38,7 +38,7 @@ export class PersonEntity {
name!: string; name!: string;
@Column({ type: 'date', nullable: true }) @Column({ type: 'date', nullable: true })
birthDate!: string | null; birthDate!: Date | string | null;
@Column({ default: '' }) @Column({ default: '' })
thumbnailPath!: string; thumbnailPath!: string;

View File

@ -2,34 +2,7 @@
-- LibraryRepository.get -- LibraryRepository.get
select select
"libraries".*, "libraries".*
(
select
to_json(obj)
from
(
select
"users"."id",
"users"."email",
"users"."createdAt",
"users"."profileImagePath",
"users"."isAdmin",
"users"."shouldChangePassword",
"users"."deletedAt",
"users"."oauthId",
"users"."updatedAt",
"users"."storageLabel",
"users"."name",
"users"."quotaSizeInBytes",
"users"."quotaUsageInBytes",
"users"."status",
"users"."profileChangedAt"
from
"users"
where
"users"."id" = "libraries"."ownerId"
) as obj
) as "owner"
from from
"libraries" "libraries"
where where
@ -38,34 +11,7 @@ where
-- LibraryRepository.getAll -- LibraryRepository.getAll
select select
"libraries".*, "libraries".*
(
select
to_json(obj)
from
(
select
"users"."id",
"users"."email",
"users"."createdAt",
"users"."profileImagePath",
"users"."isAdmin",
"users"."shouldChangePassword",
"users"."deletedAt",
"users"."oauthId",
"users"."updatedAt",
"users"."storageLabel",
"users"."name",
"users"."quotaSizeInBytes",
"users"."quotaUsageInBytes",
"users"."status",
"users"."profileChangedAt"
from
"users"
where
"users"."id" = "libraries"."ownerId"
) as obj
) as "owner"
from from
"libraries" "libraries"
where where
@ -75,34 +21,7 @@ order by
-- LibraryRepository.getAllDeleted -- LibraryRepository.getAllDeleted
select select
"libraries".*, "libraries".*
(
select
to_json(obj)
from
(
select
"users"."id",
"users"."email",
"users"."createdAt",
"users"."profileImagePath",
"users"."isAdmin",
"users"."shouldChangePassword",
"users"."deletedAt",
"users"."oauthId",
"users"."updatedAt",
"users"."storageLabel",
"users"."name",
"users"."quotaSizeInBytes",
"users"."quotaUsageInBytes",
"users"."status",
"users"."profileChangedAt"
from
"users"
where
"users"."id" = "libraries"."ownerId"
) as obj
) as "owner"
from from
"libraries" "libraries"
where where

View File

@ -1,31 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely'; import { Insertable, Kysely, sql, Updateable } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { DB, Libraries } from 'src/db'; import { DB, Libraries } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
import { LibraryEntity } from 'src/entities/library.entity';
import { AssetType } from 'src/enum'; import { AssetType } from 'src/enum';
const userColumns = [
'users.id',
'users.email',
'users.createdAt',
'users.profileImagePath',
'users.isAdmin',
'users.shouldChangePassword',
'users.deletedAt',
'users.oauthId',
'users.updatedAt',
'users.storageLabel',
'users.name',
'users.quotaSizeInBytes',
'users.quotaUsageInBytes',
'users.status',
'users.profileChangedAt',
] as const;
export enum AssetSyncResult { export enum AssetSyncResult {
DO_NOTHING, DO_NOTHING,
UPDATE, UPDATE,
@ -33,72 +13,59 @@ export enum AssetSyncResult {
CHECK_OFFLINE, CHECK_OFFLINE,
} }
const withOwner = (eb: ExpressionBuilder<DB, 'libraries'>) => {
return jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'libraries.ownerId').select(userColumns)).as(
'owner',
);
};
@Injectable() @Injectable()
export class LibraryRepository { export class LibraryRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
get(id: string, withDeleted = false): Promise<LibraryEntity | undefined> { get(id: string, withDeleted = false) {
return this.db return this.db
.selectFrom('libraries') .selectFrom('libraries')
.selectAll('libraries') .selectAll('libraries')
.select(withOwner)
.where('libraries.id', '=', id) .where('libraries.id', '=', id)
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null)) .$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
.executeTakeFirst() as Promise<LibraryEntity | undefined>; .executeTakeFirst();
} }
@GenerateSql({ params: [] }) @GenerateSql({ params: [] })
getAll(withDeleted = false): Promise<LibraryEntity[]> { getAll(withDeleted = false) {
return this.db return this.db
.selectFrom('libraries') .selectFrom('libraries')
.selectAll('libraries') .selectAll('libraries')
.select(withOwner)
.orderBy('createdAt', 'asc') .orderBy('createdAt', 'asc')
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null)) .$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
.execute() as unknown as Promise<LibraryEntity[]>; .execute();
} }
@GenerateSql() @GenerateSql()
getAllDeleted(): Promise<LibraryEntity[]> { getAllDeleted() {
return this.db return this.db
.selectFrom('libraries') .selectFrom('libraries')
.selectAll('libraries') .selectAll('libraries')
.select(withOwner)
.where('libraries.deletedAt', 'is not', null) .where('libraries.deletedAt', 'is not', null)
.orderBy('createdAt', 'asc') .orderBy('createdAt', 'asc')
.execute() as unknown as Promise<LibraryEntity[]>; .execute();
} }
create(library: Insertable<Libraries>): Promise<LibraryEntity> { create(library: Insertable<Libraries>) {
return this.db return this.db.insertInto('libraries').values(library).returningAll().executeTakeFirstOrThrow();
.insertInto('libraries')
.values(library)
.returningAll()
.executeTakeFirstOrThrow() as Promise<LibraryEntity>;
} }
async delete(id: string): Promise<void> { async delete(id: string) {
await this.db.deleteFrom('libraries').where('libraries.id', '=', id).execute(); await this.db.deleteFrom('libraries').where('libraries.id', '=', id).execute();
} }
async softDelete(id: string): Promise<void> { async softDelete(id: string) {
await this.db.updateTable('libraries').set({ deletedAt: new Date() }).where('libraries.id', '=', id).execute(); await this.db.updateTable('libraries').set({ deletedAt: new Date() }).where('libraries.id', '=', id).execute();
} }
update(id: string, library: Updateable<Libraries>): Promise<LibraryEntity> { update(id: string, library: Updateable<Libraries>) {
return this.db return this.db
.updateTable('libraries') .updateTable('libraries')
.set(library) .set(library)
.where('libraries.id', '=', id) .where('libraries.id', '=', id)
.returningAll() .returningAll()
.executeTakeFirstOrThrow() as Promise<LibraryEntity>; .executeTakeFirstOrThrow();
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })

View File

@ -63,6 +63,14 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
Name?: string; Name?: string;
}[]; }[];
}; };
Device?: {
Manufacturer?: string;
ModelName?: string;
};
AndroidMake?: string;
AndroidModel?: string;
} }
@Injectable() @Injectable()

View File

@ -1,6 +1,6 @@
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
import { isString } from 'class-validator'; import { isString } from 'class-validator';
import cookieParser from 'cookie'; import { parse } from 'cookie';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { IncomingHttpHeaders } from 'node:http'; import { IncomingHttpHeaders } from 'node:http';
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
@ -287,7 +287,7 @@ export class AuthService extends BaseService {
} }
private getCookieToken(headers: IncomingHttpHeaders): string | null { private getCookieToken(headers: IncomingHttpHeaders): string | null {
const cookies = cookieParser.parse(headers.cookie || ''); const cookies = parse(headers.cookie || '');
return cookies[ImmichCookie.ACCESS_TOKEN] || null; return cookies[ImmichCookie.ACCESS_TOKEN] || null;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1081,6 +1081,7 @@ describe(MetadataService.name, () => {
}), }),
); );
}); });
it('should handle valid negative rating value', async () => { it('should handle valid negative rating value', async () => {
mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
mockReadTags({ Rating: -1 }); mockReadTags({ Rating: -1 });
@ -1193,6 +1194,23 @@ describe(MetadataService.name, () => {
type: 'VIDEO', type: 'VIDEO',
}); });
}); });
it.each([
{ Make: '1', Model: '2', Device: { Manufacturer: '3', ModelName: '4' }, AndroidMake: '4', AndroidModel: '5' },
{ Device: { Manufacturer: '1', ModelName: '2' }, AndroidMake: '3', AndroidModel: '4' },
{ AndroidMake: '1', AndroidModel: '2' },
])('should read camera make and model correct place %s', async (metaData) => {
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
mockReadTags(metaData);
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
expect.objectContaining({
make: '1',
model: '2',
}),
);
});
}); });
describe('handleQueueSidecar', () => { describe('handleQueueSidecar', () => {

View File

@ -221,8 +221,8 @@ export class MetadataService extends BaseService {
colorspace: exifTags.ColorSpace ?? null, colorspace: exifTags.ColorSpace ?? null,
// camera // camera
make: exifTags.Make ?? null, make: exifTags.Make ?? exifTags?.Device?.Manufacturer ?? exifTags.AndroidMake ?? null,
model: exifTags.Model ?? null, model: exifTags.Model ?? exifTags?.Device?.ModelName ?? exifTags.AndroidModel ?? null,
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
iso: validate(exifTags.ISO) as number, iso: validate(exifTags.ISO) as number,
exposureTime: exifTags.ExposureTime ?? null, exposureTime: exifTags.ExposureTime ?? null,

View File

@ -222,7 +222,7 @@ describe(PersonService.name, () => {
mocks.person.update.mockResolvedValue(personStub.withBirthDate); mocks.person.update.mockResolvedValue(personStub.withBirthDate);
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.update(authStub.admin, 'person-1', { birthDate: '1976-06-30' })).resolves.toEqual({ await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({
id: 'person-1', id: 'person-1',
name: 'Person 1', name: 'Person 1',
birthDate: '1976-06-30', birthDate: '1976-06-30',
@ -231,7 +231,7 @@ describe(PersonService.name, () => {
isFavorite: false, isFavorite: false,
updatedAt: expect.any(Date), updatedAt: expect.any(Date),
}); });
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' }); expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));

3
server/src/utils/date.ts Normal file
View File

@ -0,0 +1,3 @@
export const asDateString = (x: Date | string | null): string | null => {
return x instanceof Date ? x.toISOString().split('T')[0] : x;
};

View File

@ -6,7 +6,6 @@ import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
import { StorageAsset } from 'src/types'; import { StorageAsset } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub'; import { fileStub } from 'test/fixtures/file.stub';
import { libraryStub } from 'test/fixtures/library.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
const previewFile: AssetFileEntity = { const previewFile: AssetFileEntity = {
@ -396,7 +395,6 @@ export const assetStub = {
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
libraryId: 'library-id', libraryId: 'library-id',
library: libraryStub.externalLibrary1,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@ -751,7 +749,6 @@ export const assetStub = {
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
libraryId: 'library-id', libraryId: 'library-id',
library: libraryStub.externalLibrary1,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'photo.jpg', originalFileName: 'photo.jpg',

View File

@ -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/**'],
}),
};

View File

@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { Asset, AuthUser, User } from 'src/database'; import { Asset, AuthUser, Library, User } from 'src/database';
import { OnThisDayData } from 'src/entities/memory.entity'; import { OnThisDayData } from 'src/entities/memory.entity';
import { AssetStatus, AssetType, MemoryType } from 'src/enum'; import { AssetStatus, AssetType, MemoryType } from 'src/enum';
import { ActivityItem, MemoryItem } from 'src/types'; import { ActivityItem, MemoryItem } from 'src/types';
@ -13,7 +13,11 @@ export const newDate = () => new Date();
export const newUpdateId = () => 'uuid-v7'; export const newUpdateId = () => 'uuid-v7';
export const newSha1 = () => Buffer.from('this is a fake hash'); export const newSha1 = () => Buffer.from('this is a fake hash');
const authUser = (authUser: Partial<AuthUser>) => ({ const authFactory = (user: Partial<AuthUser> = {}) => ({
user: authUserFactory(user),
});
const authUserFactory = (authUser: Partial<AuthUser>) => ({
id: newUuid(), id: newUuid(),
isAdmin: false, isAdmin: false,
name: 'Test User', name: 'Test User',
@ -23,7 +27,7 @@ const authUser = (authUser: Partial<AuthUser>) => ({
...authUser, ...authUser,
}); });
const user = (user: Partial<User>) => ({ const userFactory = (user: Partial<User> = {}) => ({
id: newUuid(), id: newUuid(),
name: 'Test User', name: 'Test User',
email: 'test@immich.cloud', email: 'test@immich.cloud',
@ -32,75 +36,95 @@ const user = (user: Partial<User>) => ({
...user, ...user,
}); });
export const factory = { const assetFactory = (asset: Partial<Asset> = {}) => ({
auth: (user: Partial<AuthUser> = {}) => ({ id: newUuid(),
user: authUser(user), createdAt: newDate(),
}), updatedAt: newDate(),
authUser, deletedAt: null,
user, updateId: newUpdateId(),
asset: (asset: Partial<Asset> = {}) => ({ status: AssetStatus.ACTIVE,
id: newUuid(), checksum: newSha1(),
createdAt: newDate(), deviceAssetId: '',
updatedAt: newDate(), deviceId: '',
deletedAt: null, duplicateId: null,
updateId: newUpdateId(), duration: null,
status: AssetStatus.ACTIVE, encodedVideoPath: null,
checksum: newSha1(), fileCreatedAt: newDate(),
deviceAssetId: '', fileModifiedAt: newDate(),
deviceId: '', isArchived: false,
duplicateId: null, isExternal: false,
duration: null, isFavorite: false,
encodedVideoPath: null, isOffline: false,
fileCreatedAt: newDate(), isVisible: true,
fileModifiedAt: newDate(), libraryId: null,
isArchived: false, livePhotoVideoId: null,
isExternal: false, localDateTime: newDate(),
isFavorite: false, originalFileName: 'IMG_123.jpg',
isOffline: false, originalPath: `upload/12/34/IMG_123.jpg`,
isVisible: true, ownerId: newUuid(),
libraryId: null, sidecarPath: null,
livePhotoVideoId: null, stackId: null,
localDateTime: newDate(), thumbhash: null,
originalFileName: 'IMG_123.jpg', type: AssetType.IMAGE,
originalPath: `upload/12/34/IMG_123.jpg`, ...asset,
ownerId: newUuid(), });
sidecarPath: null,
stackId: null, const activityFactory = (activity: Partial<ActivityItem> = {}) => {
thumbhash: null, const userId = activity.userId || newUuid();
type: AssetType.IMAGE, return {
...asset,
}),
activity: (activity: Partial<ActivityItem> = {}) => {
const userId = activity.userId || newUuid();
return {
id: newUuid(),
comment: null,
isLiked: false,
userId,
user: user({ id: userId }),
assetId: newUuid(),
albumId: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUpdateId(),
...activity,
};
},
memory: (memory: Partial<MemoryItem> = {}) => ({
id: newUuid(), id: newUuid(),
comment: null,
isLiked: false,
userId,
user: userFactory({ id: userId }),
assetId: newUuid(),
albumId: newUuid(),
createdAt: newDate(), createdAt: newDate(),
updatedAt: newDate(), updatedAt: newDate(),
updateId: newUpdateId(), updateId: newUpdateId(),
deletedAt: null, ...activity,
ownerId: newUuid(), };
type: MemoryType.ON_THIS_DAY, };
data: { year: 2024 } as OnThisDayData,
isSaved: false, const libraryFactory = (library: Partial<Library> = {}) => ({
memoryAt: newDate(), id: newUuid(),
seenAt: null, createdAt: newDate(),
showAt: newDate(), updatedAt: newDate(),
hideAt: newDate(), updateId: newUpdateId(),
assets: [], deletedAt: null,
...memory, refreshedAt: null,
}), name: 'Library',
assets: [],
ownerId: newUuid(),
importPaths: [],
exclusionPatterns: [],
...library,
});
const memoryFactory = (memory: Partial<MemoryItem> = {}) => ({
id: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUpdateId(),
deletedAt: null,
ownerId: newUuid(),
type: MemoryType.ON_THIS_DAY,
data: { year: 2024 } as OnThisDayData,
isSaved: false,
memoryAt: newDate(),
seenAt: null,
showAt: newDate(),
hideAt: newDate(),
assets: [],
...memory,
});
export const factory = {
activity: activityFactory,
asset: assetFactory,
auth: authFactory,
authUser: authUserFactory,
library: libraryFactory,
memory: memoryFactory,
user: userFactory,
}; };

View File

@ -65,7 +65,7 @@
widthStyle="100%" widthStyle="100%"
/> />
{#if person.isFavorite} {#if person.isFavorite}
<div class="absolute top-2 left-2"> <div class="absolute top-4 left-4">
<Icon path={mdiHeart} size="24" class="text-white" /> <Icon path={mdiHeart} size="24" class="text-white" />
</div> </div>
{/if} {/if}

View File

@ -50,9 +50,11 @@
<button <button
type="button" type="button"
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100" class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
title={$t('previous')}
aria-label={$t('previous')}
onclick={scrollLeft} onclick={scrollLeft}
> >
<Icon path={mdiChevronLeft} size="36" /></button <Icon path={mdiChevronLeft} size="36" ariaLabel={$t('previous')} /></button
> >
</div> </div>
{/if} {/if}
@ -61,9 +63,11 @@
<button <button
type="button" type="button"
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100" class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
title={$t('next')}
aria-label={$t('next')}
onclick={scrollRight} onclick={scrollRight}
> >
<Icon path={mdiChevronRight} size="36" /></button <Icon path={mdiChevronRight} size="36" ariaLabel={$t('next')} /></button
> >
</div> </div>
{/if} {/if}

View File

@ -32,6 +32,7 @@
let showFilter = $state(false); let showFilter = $state(false);
let isSearchSuggestions = $state(false); let isSearchSuggestions = $state(false);
let selectedId: string | undefined = $state(); let selectedId: string | undefined = $state();
let isFocus = $state(false);
const listboxId = generateId(); const listboxId = generateId();
@ -98,7 +99,25 @@
}; };
const onSubmit = () => { const onSubmit = () => {
handlePromiseError(handleSearch({ query: value })); const searchType = getSearchType();
let payload: SmartSearchDto | MetadataSearchDto = {} as SmartSearchDto | MetadataSearchDto;
switch (searchType) {
case 'smart': {
payload = { query: value } as SmartSearchDto;
break;
}
case 'metadata': {
payload = { originalFileName: value } as MetadataSearchDto;
break;
}
case 'description': {
payload = { description: value } as MetadataSearchDto;
break;
}
}
handlePromiseError(handleSearch(payload));
saveSearchTerm(value); saveSearchTerm(value);
}; };
@ -132,10 +151,12 @@
const openDropdown = () => { const openDropdown = () => {
showSuggestions = true; showSuggestions = true;
isFocus = true;
}; };
const closeDropdown = () => { const closeDropdown = () => {
showSuggestions = false; showSuggestions = false;
isFocus = false;
searchHistoryBox?.clearSelection(); searchHistoryBox?.clearSelection();
}; };
@ -143,6 +164,26 @@
event.preventDefault(); event.preventDefault();
onSubmit(); onSubmit();
}; };
function getSearchType(): 'smart' | 'metadata' | 'description' {
const t = localStorage.getItem('searchQueryType');
return t === 'smart' || t === 'description' ? t : 'metadata';
}
function getSearchTypeText(): string {
const searchType = getSearchType();
switch (searchType) {
case 'smart': {
return $t('context');
}
case 'metadata': {
return $t('filename');
}
case 'description': {
return $t('description');
}
}
}
</script> </script>
<svelte:window <svelte:window
@ -214,6 +255,21 @@
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all"> <div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all">
<CircleIconButton title={$t('show_search_options')} icon={mdiTune} onclick={onFilterClick} size="20" /> <CircleIconButton title={$t('show_search_options')} icon={mdiTune} onclick={onFilterClick} size="20" />
</div> </div>
{#if isFocus}
<div
class="absolute inset-y-0 flex items-center"
class:right-16={isFocus}
class:right-28={isFocus && value.length > 0}
>
<p
class="bg-immich-primary text-white dark:bg-immich-dark-primary/90 dark:text-black/75 rounded-full px-3 py-1 text-xs z-10"
>
{getSearchTypeText()}
</p>
</div>
{/if}
{#if showClearIcon} {#if showClearIcon}
<div class="absolute inset-y-0 right-0 flex items-center pr-2"> <div class="absolute inset-y-0 right-0 flex items-center pr-2">
<CircleIconButton onclick={onClear} icon={mdiClose} title={$t('clear')} size="20" /> <CircleIconButton onclick={onClear} icon={mdiClose} title={$t('clear')} size="20" />

View File

@ -2,7 +2,7 @@
import type { SearchLocationFilter } from './search-location-section.svelte'; import type { SearchLocationFilter } from './search-location-section.svelte';
import type { SearchDisplayFilters } from './search-display-section.svelte'; import type { SearchDisplayFilters } from './search-display-section.svelte';
import type { SearchDateFilter } from './search-date-section.svelte'; import type { SearchDateFilter } from './search-date-section.svelte';
import { MediaType } from '$lib/constants'; import { MediaType, QueryType, validQueryTypes } from '$lib/constants';
export type SearchFilter = { export type SearchFilter = {
query: string; query: string;
@ -55,9 +55,18 @@
return value === null ? undefined : value; return value === null ? undefined : value;
} }
function storeQueryType(type: SearchFilter['queryType']) {
localStorage.setItem('searchQueryType', type);
}
function defaultQueryType(): QueryType {
const storedQueryType = localStorage.getItem('searchQueryType') as QueryType;
return validQueryTypes.has(storedQueryType) ? storedQueryType : QueryType.SMART;
}
let filter: SearchFilter = $state({ let filter: SearchFilter = $state({
query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '', query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '',
queryType: 'smart', queryType: defaultQueryType(),
personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []), personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []),
tagIds: new SvelteSet('tagIds' in searchQuery ? searchQuery.tagIds : []), tagIds: new SvelteSet('tagIds' in searchQuery ? searchQuery.tagIds : []),
location: { location: {
@ -90,7 +99,7 @@
const resetForm = () => { const resetForm = () => {
filter = { filter = {
query: '', query: '',
queryType: 'smart', queryType: defaultQueryType(), // retain from localStorage or default
personIds: new SvelteSet(), personIds: new SvelteSet(),
tagIds: new SvelteSet(), tagIds: new SvelteSet(),
location: {}, location: {},
@ -142,8 +151,14 @@
const onsubmit = (event: Event) => { const onsubmit = (event: Event) => {
event.preventDefault(); event.preventDefault();
storeQueryType(filter.queryType);
search(); search();
}; };
// Will be called whenever queryType changes, not just onsubmit.
$effect(() => {
storeQueryType(filter.queryType);
});
</script> </script>
<FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}> <FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}>

View File

@ -119,6 +119,14 @@ export const fallbackLocale = {
name: 'English (US)', name: 'English (US)',
}; };
export enum QueryType {
SMART = 'smart',
METADATA = 'metadata',
DESCRIPTION = 'description',
}
export const validQueryTypes = new Set([QueryType.SMART, QueryType.METADATA, QueryType.DESCRIPTION]);
export const locales = [ export const locales = [
{ code: 'af-ZA', name: 'Afrikaans (South Africa)' }, { code: 'af-ZA', name: 'Afrikaans (South Africa)' },
{ code: 'sq-AL', name: 'Albanian (Albania)' }, { code: 'sq-AL', name: 'Albanian (Albania)' },