1
0
mirror of https://github.com/immich-app/immich.git synced 2025-03-11 15:09:45 +02:00

Merge branch 'main' into new-asset-sync

This commit is contained in:
Alex 2025-03-10 12:04:01 -05:00 committed by GitHub
commit d3de6475bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
146 changed files with 6606 additions and 6759 deletions

View File

@ -32,5 +32,5 @@ The `/api/something` endpoint is now `/api/something-else`
- [ ] I have confirmed that any new dependencies are strictly necessary.
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services`)
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)

View File

@ -41,8 +41,8 @@ jobs:
with:
token: ${{ steps.generate-token.outputs.token }}
- name: Install Poetry
run: pipx install poetry
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Bump version
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
@ -74,7 +74,7 @@ jobs:
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:

View File

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

View File

@ -1,11 +1,11 @@
<p align="center">
<br/>
<br/>
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
<a href="https://discord.immich.app">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a>
<br/>
<br/>
<br/>
<br/>
</p>
<p align="center">
@ -63,7 +63,7 @@
Access the demo [here](https://demo.immich.app). The demo is running on a Free-tier Oracle VM in Amsterdam with a 2.4Ghz quad-core ARM64 CPU and 24GB RAM.
For the mobile app, you can use `https://demo.immich.app/api` for the `Server Endpoint URL`
For the mobile app, you can use `https://demo.immich.app` for the `Server Endpoint URL`
### Login credentials

View File

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

View File

@ -117,7 +117,7 @@ See [Backup and Restore](/docs/administration/backup-and-restore.md).
### Does Immich support reading existing face tag metadata?
No, it currently does not. There is an [open feature request on GitHub](https://github.com/immich-app/immich/discussions/4348).
Yes, it creates new faces and persons from the imported asset metadata. For details see the [feature request #4348](https://github.com/immich-app/immich/discussions/4348) and [PR #6455](https://github.com/immich-app/immich/pull/6455).
### Does Immich support the filtering of NSFW images?

View File

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

View File

@ -77,7 +77,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata`). If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
<img
src={require('./img/unraid05.webp').default}

View File

@ -18377,9 +18377,9 @@
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

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

View File

@ -240,7 +240,7 @@
"storage_template_hash_verification_enabled_description": "Enables hash verification, don't disable this unless you're certain of the implications",
"storage_template_migration": "Storage template migration",
"storage_template_migration_description": "Apply the current <link>{template}</link> to previously uploaded assets",
"storage_template_migration_info": "Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the <link>{job}</link>.",
"storage_template_migration_info": "The storage template will convert all extensions to lowercase. Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the <link>{job}</link>.",
"storage_template_migration_job": "Storage Template Migration Job",
"storage_template_more_details": "For more details about this feature, refer to the <template-link>Storage Template</template-link> and its <implications-link>implications</implications-link>",
"storage_template_onboarding_description": "When enabled, this feature will auto-organize files based on a user-defined template. Due to stability issues the feature has been turned off by default. For more information, please see the <link>documentation</link>.",
@ -987,6 +987,7 @@
"permanently_deleted_asset": "Permanently deleted asset",
"permanently_deleted_assets_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}",
"person": "Person",
"person_birthdate": "Born on {date}",
"person_hidden": "{name}{hidden, select, true { (hidden)} other {}}",
"photo_shared_all_users": "Looks like you shared your photos with all users or you don't have any user to share with.",
"photos": "Photos",

View File

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

View File

@ -5,13 +5,12 @@
# Setup
This project uses [Poetry](https://python-poetry.org/docs/#installation), so be sure to install it first.
Running `poetry install --no-root --with dev --with cpu` will install everything you need in an isolated virtual environment.
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
To add or remove dependencies, you can use the commands `poetry add $PACKAGE_NAME` and `poetry remove $PACKAGE_NAME`, respectively.
Be sure to commit the `poetry.lock` and `pyproject.toml` files with `poetry lock --no-update` to reflect any changes in dependencies.
This project uses [uv](https://docs.astral.sh/uv/getting-started/installation/), so be sure to install it first.
Running `uv sync --extra cpu` will install everything you need in an isolated virtual environment.
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--group cpu` with either of `--group cuda` or `--group openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
To add or remove dependencies, you can use the commands `uv add $PACKAGE_NAME` and `uv remove $PACKAGE_NAME`, respectively.
Be sure to commit the `uv.lock` and `pyproject.toml` files with `uv lock` to reflect any changes in dependencies.
# Load Testing
@ -19,22 +18,25 @@ To measure inference throughput and latency, you can use [Locust](https://locust
Locust works by querying the model endpoints and aggregating their statistics, meaning the app must be deployed.
You can change the models or adjust options like score thresholds through the Locust UI.
To get started, you can simply run `locust --web-host 127.0.0.1` and open `localhost:8089` in a browser to access the UI. See the [Locust documentation](https://docs.locust.io/en/stable/index.html) for more info on running Locust.
To get started, you can simply run `locust --web-host 127.0.0.1` and open `localhost:8089` in a browser to access the UI. See the [Locust documentation](https://docs.locust.io/en/stable/index.html) for more info on running Locust.
Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.
# Facial Recognition
## Acknowledgements
This project utilizes facial recognition models from the [InsightFace](https://github.com/deepinsight/insightface/tree/master/model_zoo) project. We appreciate the work put into developing these models, which have been beneficial to the machine learning part of this project.
### Used Models
* antelopev2
* buffalo_l
* buffalo_m
* buffalo_s
- antelopev2
- buffalo_l
- buffalo_m
- buffalo_s
## License and Use Restrictions
We have received permission to use the InsightFace facial recognition models in our project, as granted via email by Jia Guo (guojia@insightface.ai) on 18th March 2023. However, it's important to note that this permission does not extend to the redistribution or commercial use of their models by third parties. Users and developers interested in using these models should review the licensing terms provided in the InsightFace GitHub repository.
For more information on the capabilities of the InsightFace models and to ensure compliance with their license, please refer to their [official repository](https://github.com/deepinsight/insightface). Adhering to the specified licensing terms is crucial for the respectful and lawful use of their work.
For more information on the capabilities of the InsightFace models and to ensure compliance with their license, please refer to their [official repository](https://github.com/deepinsight/insightface). Adhering to the specified licensing terms is crucial for the respectful and lawful use of their work.

File diff suppressed because it is too large Load Diff

View File

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

2648
machine-learning/uv.lock generated Normal file

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 e2e version "$SERVER_PUMP"
npm --prefix e2e i --package-lock-only
poetry --directory machine-learning version "$SERVER_PUMP"
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "$SERVER_PUMP"
fi
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then

View File

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

View File

@ -221,7 +221,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setSmallIcon(R.drawable.notification_icon)
.build()
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
}
@ -260,7 +260,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
var builder = if (isDetail) notificationDetailBuilder else notificationBuilder
if (builder == null) {
builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setSmallIcon(R.drawable.notification_icon)
.setOnlyAlertOnce(true)
.setOngoing(true)
if (isDetail) {

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/utils/map_utils.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
extension MapMarkers on MaplibreMapController {
extension MapMarkers on MapLibreMapController {
static var _completer = Completer()..complete();
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
@ -40,12 +40,26 @@ extension MapMarkers on MaplibreMapController {
await addGeoJSONSourceForMarkers(markers);
await addHeatmapLayer(
await addCircleLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
MapUtils.defaultHeatMapLayerProperties,
const CircleLayerProperties(
circleRadius: 10,
circleColor: "rgba(150,86,34,0.7)",
circleBlur: 1.0,
circleOpacity: 0.7,
circleStrokeWidth: 0.1,
circleStrokeColor: "rgba(203,46,19,0.5)",
circleStrokeOpacity: 0.7,
),
);
// await addHeatmapLayer(
// MapUtils.defaultSourceId,
// MapUtils.defaultHeatMapLayerId,
// MapUtils.defaultHeatMapLayerProperties,
// );
_completer.complete();
}

View File

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

View File

@ -1,8 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
@ -16,6 +18,8 @@ class PeopleCollectionPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final people = ref.watch(getAllPeopleProvider);
final headers = ApiService.getRequestHeaders();
final formFocus = useFocusNode();
final ValueNotifier<String?> search = useState(null);
showNameEditModel(
String personId,
@ -36,10 +40,70 @@ class PeopleCollectionPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
title: Text('people'.tr()),
automaticallyImplyLeading: search.value == null,
title: search.value != null
? TextField(
focusNode: formFocus,
onTapOutside: (_) => formFocus.unfocus(),
onChanged: (value) => search.value = value,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(left: 24),
filled: true,
fillColor: context.primaryColor.withOpacity(0.1),
hintStyle: context.textTheme.bodyLarge?.copyWith(
color: context.themeData.colorScheme.onSurfaceSecondary,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: BorderSide(
color: context.colorScheme.surfaceContainerHighest,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: BorderSide(
color: context.colorScheme.surfaceContainerHighest,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: BorderSide(
color: context.colorScheme.surfaceContainerHighest,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: BorderSide(
color: context.colorScheme.primary.withAlpha(150),
),
),
prefixIcon: Icon(
Icons.search_rounded,
color: context.colorScheme.primary,
),
hintText: 'search_filter_people_hint'.tr(),
),
autofocus: true,
)
: Text('people'.tr()),
actions: [
IconButton(
icon: Icon(search.value != null ? Icons.close : Icons.search),
onPressed: () {
search.value = search.value == null ? '' : null;
},
),
],
),
body: people.when(
data: (people) {
if (search.value != null) {
people = people.where((person) {
return person.name
.toLowerCase()
.contains(search.value!.toLowerCase());
}).toList();
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isTablet ? 6 : 3,

View File

@ -11,7 +11,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
import 'package:immich_mobile/models/map/map_event.model.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart';
@ -39,7 +38,7 @@ class MapPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final mapController = useRef<MaplibreMapController?>(null);
final mapController = useRef<MapLibreMapController?>(null);
final markers = useRef<List<MapMarker>>([]);
final markersInBounds = useRef<List<MapMarker>>([]);
final bottomSheetStreamController = useStreamController<MapEvent>();
@ -162,7 +161,7 @@ class MapPage extends HookConsumerWidget {
}
}
void onMapCreated(MaplibreMapController controller) async {
void onMapCreated(MapLibreMapController controller) async {
mapController.value = controller;
controller.addListener(() {
if (controller.isCameraMoving && selectedMarker.value != null) {
@ -389,7 +388,7 @@ class _MapWithMarker extends StatelessWidget {
child: Stack(
children: [
style.widgetWhen(
onData: (style) => MaplibreMap(
onData: (style) => MapLibreMap(
initialCameraPosition:
const CameraPosition(target: LatLng(0, 0)),
styleString: style,
@ -403,7 +402,7 @@ class _MapWithMarker extends StatelessWidget {
tiltGesturesEnabled: false,
dragEnabled: false,
myLocationEnabled: false,
attributionButtonPosition: AttributionButtonPosition.TopRight,
attributionButtonPosition: AttributionButtonPosition.topRight,
rotateGesturesEnabled: false,
),
),

View File

@ -24,7 +24,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedLatLng = useValueNotifier<LatLng>(initialLatLng);
final controller = useRef<MaplibreMapController?>(null);
final controller = useRef<MapLibreMapController?>(null);
final marker = useRef<Symbol?>(null);
Future<void> onStyleLoaded() async {
@ -74,7 +74,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
bottomRight: Radius.circular(40),
),
),
child: MaplibreMap(
child: MapLibreMap(
initialCameraPosition:
CameraPosition(target: initialLatLng, zoom: 12),
styleString: style,

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>> {
/// The parameter `albumId` of this provider.
String get albumId;
@ -206,4 +208,4 @@ class _AlbumActivityProviderElement
String? get assetId => (origin as AlbumActivityProvider).assetId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

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/services/activity.service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -5,5 +6,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity_service.provider.g.dart';
@riverpod
ActivityService activityService(ActivityServiceRef ref) =>
ActivityService activityService(Ref ref) =>
ActivityService(ref.watch(activityApiRepositoryProvider));

View File

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

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> {
/// The parameter `albumId` of this provider.
String get albumId;
@ -205,4 +207,4 @@ class _ActivityStatisticsProviderElement
String? get assetId => (origin as ActivityStatisticsProvider).assetId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
on AutoDisposeAsyncNotifierProviderRef<List<PersonWithFacesResponseDto>> {
/// The parameter `asset` of this provider.
@ -186,4 +188,4 @@ class _AssetPeopleNotifierProviderElement
Asset get asset => (origin as AssetPeopleNotifierProvider).asset;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -39,6 +39,6 @@ final assetStackStateProvider = StateNotifierProvider.autoDispose
);
@riverpod
int assetStackIndex(AssetStackIndexRef ref, Asset asset) {
int assetStackIndex(Ref ref, Asset asset) {
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> {
/// The parameter `asset` of this provider.
Asset get asset;
@ -155,4 +157,4 @@ class _AssetStackIndexProviderElement extends AutoDisposeProviderElement<int>
Asset get asset => (origin as AssetStackIndexProvider).asset;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@ -6,5 +7,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'exif.provider.g.dart';
@Riverpod(keepAlive: true)
IExifInfoRepository exifRepository(ExifRepositoryRef ref) =>
IExifInfoRepository exifRepository(Ref ref) =>
IsarExifRepository(ref.watch(isarProvider));

View File

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

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/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@ -6,5 +7,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'store.provider.g.dart';
@riverpod
IStoreRepository storeRepository(StoreRepositoryRef ref) =>
IStoreRepository storeRepository(Ref ref) =>
IsarStoreRepository(ref.watch(isarProvider));

View File

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

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

View File

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

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/providers/api.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -5,5 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'map_service.provider.g.dart';
@riverpod
MapSerivce mapService(MapServiceRef ref) =>
MapSerivce(ref.watch(apiServiceProvider));
MapSerivce mapService(Ref ref) => MapSerivce(ref.watch(apiServiceProvider));

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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>> {
/// The parameter `type` of this provider.
SearchSuggestionType get type;
@ -226,4 +228,4 @@ class _GetSearchSuggestionsProviderElement
String? get model => (origin as GetSearchSuggestionsProvider).model;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

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

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

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/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
@ -11,7 +12,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'person.service.g.dart';
@riverpod
PersonService personService(PersonServiceRef ref) => PersonService(
PersonService personService(Ref ref) => PersonService(
ref.watch(personApiRepositoryProvider),
ref.watch(assetApiRepositoryProvider),
ref.read(assetRepositoryProvider),

View File

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

View File

@ -58,12 +58,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
final bool success =
await ref.watch(albumProvider.notifier).deleteAlbum(album);
if (album.shared) {
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
} else {
context
.navigateTo(const TabControllerRoute(children: [LibraryRoute()]));
}
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
if (!success) {
ImmichToast.show(

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
_scrollToIndex(index + 1);
} else {

View File

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

View File

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

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

View File

@ -41,10 +41,10 @@ class MapThumbnail extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude);
final controller = useRef<MaplibreMapController?>(null);
final controller = useRef<MapLibreMapController?>(null);
final position = useValueNotifier<Point<num>?>(null);
Future<void> onMapCreated(MaplibreMapController mapController) async {
Future<void> onMapCreated(MapLibreMapController mapController) async {
controller.value = mapController;
if (assetMarkerRemoteId != null) {
// The iOS impl returns wrong toScreenLocation without the delay
@ -73,7 +73,7 @@ class MapThumbnail extends HookConsumerWidget {
alignment: Alignment.center,
children: [
style.widgetWhen(
onData: (style) => MaplibreMap(
onData: (style) => MapLibreMap(
initialCameraPosition:
CameraPosition(target: offsettedCentre, zoom: zoom),
styleString: style,

View File

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

View File

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

View File

@ -17,16 +17,16 @@ dependencies:
path_provider_ios:
photo_manager: ^3.6.1
photo_manager_image_provider: ^2.2.0
flutter_hooks: ^0.20.4
hooks_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3
flutter_hooks: ^0.21.2
hooks_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
cached_network_image: ^3.3.1
flutter_cache_manager: ^3.3.1
intl: ^0.19.0
auto_route: ^9.2.0
fluttertoast: ^8.2.4
socket_io_client: ^2.0.3+1
maplibre_gl: 0.19.0+2
maplibre_gl: ^0.21.0
geolocator: ^11.0.0 # used to move to current location in map view
flutter_udid: ^3.0.0
flutter_svg: ^2.0.9
@ -42,7 +42,7 @@ dependencies:
path_provider: ^2.1.2
collection: ^1.18.0
http_parser: ^4.0.2
flutter_web_auth: 0.6.0
flutter_web_auth_2: ^5.0.0-alpha.0
easy_image_viewer: ^1.4.0
isar:
version: *isar_version
@ -108,8 +108,8 @@ dev_dependencies:
integration_test:
sdk: flutter
custom_lint: ^0.6.4
riverpod_lint: ^2.3.7
riverpod_generator: ^2.3.9
riverpod_lint: ^2.6.1
riverpod_generator: ^2.6.1
mocktail: ^1.0.3
immich_mobile_immich_lint:
path: './immich_lint'

View File

@ -33,9 +33,9 @@
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@ -65,7 +65,7 @@ https://immich.app
https://demo.immich.app
بالنسبة لتطبيق الهاتف المحمول، يمكنك استخدام
`https://demo.immich.app/api`
`https://demo.immich.app`
ل `نقطة نهاية الخادم`
```bash title="Demo Credential"

View File

@ -60,7 +60,7 @@ Podeu trobar la documentació principal, incloent les guies d'instal·lació, a
Podeu accedir a la demostració web a https://demo.immich.app
Per a l'aplicació mòbil, podeu utilitzar `https://demo.immich.app/api` com a "URL de punt final del servidor".
Per a l'aplicació mòbil, podeu utilitzar `https://demo.immich.app` com a "URL de punt final del servidor".
```bash title="Credencials de la demo"
Les credencials

View File

@ -64,7 +64,7 @@
Die Web-Demo kannst Du unter https://demo.immich.app finden.
Die Demo läuft auf einer Free Tier Oracle VM in Amsterdam mit einer 2.4Ghz Quad-Core ARM64 CPU und 24GB RAM.
Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
Für die Handy-App kannst Du `https://demo.immich.app` als `Server Endpoint URL` angeben.
### Login Daten

View File

@ -61,7 +61,7 @@ Puedes encontrar la documentación oficial, incluidas las guías de instalación
Puedes acceder a la demostración web en <https://demo.immich.app>
Para la aplicación móvil, puedes usar `https://demo.immich.app/api` en la `URL del servidor`.
Para la aplicación móvil, puedes usar `https://demo.immich.app` en la `URL del servidor`.
```bash title="Credenciales de la demo"
Credenciales

View File

@ -61,7 +61,7 @@ Vous pouvez trouver la documentation principale ainsi que les guides d'installat
Vous pouvez accéder à la démo en ligne sur https://demo.immich.app
Pour l'application mobile, vous pouvez utiliser `https://demo.immich.app/api` dans le champ `URL du point d'accès au serveur`
Pour l'application mobile, vous pouvez utiliser `https://demo.immich.app` dans le champ `URL du point d'accès au serveur`
```bash title="Identifiants pour la démo"
Les identifiants

View File

@ -61,7 +61,7 @@ La documentazione ufficiale, inclusa la guida all'installazione, è disponibile
Prova la demo del progetto https://demo.immich.app
Sull'app mobile, imposta `https://demo.immich.app/api` come `Server Endpoint URL`
Sull'app mobile, imposta `https://demo.immich.app` come `Server Endpoint URL`
```bash title="Demo Credential"
Credenziali di accesso

View File

@ -60,7 +60,7 @@
web デモは https://demo.immich.app からアクセスできます
モバイルアプリの場合、`Server Endpoint URL` には `https://demo.immich.app/api` を使用することができます
モバイルアプリの場合、`Server Endpoint URL` には `https://demo.immich.app` を使用することができます
```bash title="Demo Credential"
The credential

View File

@ -63,7 +63,7 @@
[이곳](https://demo.immich.app)에서 데모를 체험해보세요. 데모 서버는 2.4Ghz 쿼드 코어 ARM64 CPU 및 24GB 램으로 구성된 Oracle Free-tier VM 암스테르담 리전에서 구동됩니다.
모바일 앱의 경우, `서버 엔드포인트 URL``https://demo.immich.app/api`를 입력하세요.
모바일 앱의 경우, `서버 엔드포인트 URL``https://demo.immich.app`를 입력하세요.
### 로그인 정보

View File

@ -61,7 +61,7 @@ De belangrijkste documentatie, inclusief installatie handleidingen, zijn te vind
Je kunt de demo [hier](https://demo.immich.app/) bekijken. De demo server is actief op een Free-tier Oracle VM in Amsterdam met een 2.4GHz quad-core ARM64 CPU en 24GB RAM.
Voor de mobiele app kun je gebruik maken van `https://demo.immich.app/api` voor de `Server Endpoint URL`
Voor de mobiele app kun je gebruik maken van `https://demo.immich.app` voor de `Server Endpoint URL`
### Login gegevens

View File

@ -71,7 +71,7 @@ hospedada no Nível Gratuito da Oracle VM em Amsterdam com um processador 2.4Ghz
quad-core ARM64 e 24GB de RAM.
No aplicativo para dispositivos móveis, você pode usar
`https://demo.immich.app/api` no campo `Server Endpoint URL`
`https://demo.immich.app` no campo `Server Endpoint URL`
### Credenciais de login

View File

@ -64,7 +64,7 @@
Вы можете опробовать [Web демонстрационную версию](https://demo.immich.app/)
В мобильном приложении укажите `https://demo.immich.app/api` в поле `URL-адрес сервера`
В мобильном приложении укажите `https://demo.immich.app` в поле `URL-адрес сервера`
### Данные для входа

View File

@ -62,7 +62,7 @@ Dokumentation och installationsguider hittas på https://imiich.app/.
Ett webb-demo finns att testa på https://demo.immich.app
Använd `https://demo.immich.app/api` i mobilappen som `Server Endpoint URL`
Använd `https://demo.immich.app` i mobilappen som `Server Endpoint URL`
```bash title="Inloggningsuppgifter För Demo"
Inloggsningsuppgifter

View File

@ -65,7 +65,7 @@
เข้าถึงการสาธิตได้ [ที่นี่](https://demo.immich.app) โดยการสาธิตนี้ทำงานบน Oracle VM Free-tier ตั้งอยู่ที่อัมสเตอร์ดัม ใช้ซีพียู ARM64 quad-core 2.4Ghz และแรม 24GB
สำหรับแอปมือถือ คุณสามารถใช้ `https://demo.immich.app/api` เป็น `Server Endpoint URL`
สำหรับแอปมือถือ คุณสามารถใช้ `https://demo.immich.app` เป็น `Server Endpoint URL`
### ข้อมูลการเข้าสู่ระบบ

View File

@ -60,7 +60,7 @@ Kurulum dahil olmak üzere resmi belgeleri https://immich.app/ adresinde bulabil
Web demo adresi: https://demo.immich.app
Mobil uygulama için `Server Endpoint URL` olarak `https://demo.immich.app/api` adresini kullanabilirsiniz.
Mobil uygulama için `Server Endpoint URL` olarak `https://demo.immich.app` adresini kullanabilirsiniz.
```bash title="Demo Bilgileri"
Giriş bilgileri:

View File

@ -63,7 +63,7 @@
Доступ до демо-версії [тут](https://demo.immich.app). Демоверсія працює на безкоштовному Oracle VM у Амстердамі з чотириядерним ARM64 процесором (2.4 ГГц) і 24 ГБ оперативної пам’яті.
Для мобільного додатку ви можете використовувати `https://demo.immich.app/api` в якості `Server Endpoint URL`.
Для мобільного додатку ви можете використовувати `https://demo.immich.app` в якості `Server Endpoint URL`.
### Облікові дані для входу

View File

@ -65,7 +65,7 @@
Truy cập bản demo [tại đây](https://demo.immich.app). Bản demo đang chạy trên máy ảo Oracle Free-tier ở Amsterdam với CPU ARM64 lõi tứ 2,4 GHz và RAM 24 GB.
Đối với ứng dụng di động, bạn có thể sử dụng `https://demo.immich.app/api` cho `Server Endpoint URL`
Đối với ứng dụng di động, bạn có thể sử dụng `https://demo.immich.app` cho `Server Endpoint URL`
### Thông tin đăng nhập

View File

@ -67,7 +67,7 @@
您可以在[此处](https://demo.immich.app)访问在线演示网站。该示例网站运行的机器配置为:甲骨文免费虚拟机套餐——阿姆斯特丹 4核 2.4Ghz ARM64 CPU,24GB RAM。
在移动端,您可以使用 `https://demo.immich.app/api` 作为 `服务终端链接`
在移动端,您可以使用 `https://demo.immich.app` 作为 `服务终端链接`
### 登录认证信息

3430
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,8 @@
"chokidar": "^3.5.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"exiftool-vendored": "^28.3.1",
"fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2",
@ -107,7 +108,7 @@
"@types/archiver": "^6.0.0",
"@types/async-lock": "^1.4.2",
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/js-yaml": "^4.0.9",
@ -131,7 +132,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "^16.0.0",
"kysely-codegen": "^0.17.0",
"kysely-codegen": "^0.18.0",
"mock-fs": "^5.2.0",
"node-addon-api": "^8.3.0",
"pngjs": "^7.0.0",

View File

@ -4,13 +4,13 @@ import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpda
import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { APIKeyService } from 'src/services/api-key.service';
import { ApiKeyService } from 'src/services/api-key.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('API Keys')
@Controller('api-keys')
export class APIKeyController {
constructor(private service: APIKeyService) {}
constructor(private service: ApiKeyService) {}
@Post()
@Authenticated({ permission: Permission.API_KEY_CREATE })

View File

@ -1,5 +1,5 @@
import { sql } from 'kysely';
import { Permission } from 'src/enum';
import { AssetStatus, AssetType, Permission } from 'src/enum';
export type AuthUser = {
id: string;
@ -10,11 +10,74 @@ export type AuthUser = {
quotaSizeInBytes: number | null;
};
export type Library = {
id: string;
ownerId: string;
createdAt: Date;
updatedAt: Date;
updateId: string;
name: string;
importPaths: string[];
exclusionPatterns: string[];
deletedAt: Date | null;
refreshedAt: Date | null;
assets?: Asset[];
};
export type AuthApiKey = {
id: string;
permissions: Permission[];
};
export type ApiKey = {
id: string;
name: string;
userId: string;
createdAt: Date;
updatedAt: Date;
permissions: Permission[];
};
export type User = {
id: string;
name: string;
email: string;
profileImagePath: string;
profileChangedAt: Date;
};
export type Asset = {
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
id: string;
updateId: string;
status: AssetStatus;
checksum: Buffer<ArrayBufferLike>;
deviceAssetId: string;
deviceId: string;
duplicateId: string | null;
duration: string | null;
encodedVideoPath: string | null;
fileCreatedAt: Date | null;
fileModifiedAt: Date | null;
isArchived: boolean;
isExternal: boolean;
isFavorite: boolean;
isOffline: boolean;
isVisible: boolean;
libraryId: string | null;
livePhotoVideoId: string | null;
localDateTime: Date | null;
originalFileName: string;
originalPath: string;
ownerId: string;
sidecarPath: string | null;
stackId: string | null;
thumbhash: Buffer<ArrayBufferLike> | null;
type: AssetType;
};
export type AuthSharedLink = {
id: string;
expiresAt: Date | null;

7
server/src/db.d.ts vendored
View File

@ -4,7 +4,8 @@
*/
import type { ColumnType } from 'kysely';
import { AssetType, Permission, SyncEntityType } from 'src/enum';
import { OnThisDayData } from 'src/entities/memory.entity';
import { AssetType, MemoryType, Permission, SyncEntityType } from 'src/enum';
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
@ -240,7 +241,7 @@ export interface Libraries {
export interface Memories {
createdAt: Generated<Timestamp>;
data: Json;
data: OnThisDayData;
deletedAt: Timestamp | null;
hideAt: Timestamp | null;
id: Generated<string>;
@ -249,7 +250,7 @@ export interface Memories {
ownerId: string;
seenAt: Timestamp | null;
showAt: Timestamp | null;
type: string;
type: MemoryType;
updatedAt: Generated<Timestamp>;
updateId: Generated<string>;
}

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More