Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0f444e4d | ||
|
|
6b0f91cafd | ||
|
|
3f71d2d33d | ||
|
|
f2942588f2 | ||
|
|
b47027efc2 | ||
|
|
34201be74c | ||
|
|
3e804f16df | ||
|
|
3512140148 | ||
|
|
bff6914a73 | ||
|
|
652add635f | ||
|
|
fde410e2ac | ||
|
|
f04e47803c | ||
|
|
61d74263d9 | ||
|
|
66ee065c0c | ||
|
|
09bcf6974e | ||
|
|
5d7d615433 | ||
|
|
5387048dc3 | ||
|
|
6930df71cf | ||
|
|
52bbf6da5d | ||
|
|
1cd5df7558 | ||
|
|
74429798e2 | ||
|
|
651f3ea5eb | ||
|
|
0909335d02 | ||
|
|
827e4b5f75 | ||
|
|
c8ff07fff0 | ||
|
|
4a21cb2d00 | ||
|
|
07f7fffae7 | ||
|
|
441ee2ef90 | ||
|
|
acad133e3a | ||
|
|
ef8714fda9 | ||
|
|
16171eee8d | ||
|
|
d3c1781478 | ||
|
|
329b52e670 | ||
|
|
a1b9a1d244 | ||
|
|
377cec9fb1 | ||
|
|
48b9c63268 | ||
|
|
caccb1094d | ||
|
|
43ffcf7e8f | ||
|
|
77fe2e55be | ||
|
|
a59e9e1d9e | ||
|
|
896645130b | ||
|
|
045bb855d2 | ||
|
|
3b4f6edbdb | ||
|
|
1cbf9ff621 | ||
|
|
41c2c8b82d | ||
|
|
43ec0b77a0 | ||
|
|
408fa45c51 | ||
|
|
eed1243263 | ||
|
|
8f5214724c | ||
|
|
55b6b28afb | ||
|
|
5a48034e33 | ||
|
|
756f4e5986 | ||
|
|
48492b9f4e | ||
|
|
e101e40c47 | ||
|
|
9a80a2151c | ||
|
|
73075c64d1 | ||
|
|
053a0482b4 | ||
|
|
9cdec62918 | ||
|
|
e3694695ae | ||
|
|
9a3a01ca78 | ||
|
|
f0bc318712 | ||
|
|
53adb0c515 | ||
|
|
747afa0cee | ||
|
|
104e489000 | ||
|
|
5764bf16f3 | ||
|
|
8ebac41318 | ||
|
|
a2130aa6c5 | ||
|
|
5dbf46ac3c | ||
|
|
b7d42e7e8e | ||
|
|
d08535e7f6 | ||
|
|
eb1225a0a5 | ||
|
|
284edd97d6 | ||
|
|
d1b0b64d59 | ||
|
|
d0cc231782 | ||
|
|
6ce35d47f5 | ||
|
|
d1db479727 | ||
|
|
1e748864c5 | ||
|
|
c92c442356 |
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -42,10 +42,10 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
uses: docker/setup-qemu-action@v2.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
uses: docker/setup-buildx-action@v2.7.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
uses: docker/build-push-action@v4.1.1
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -167,13 +167,13 @@ jobs:
|
||||
run: npm --prefix server run typeorm:migrations:run
|
||||
- name: Generate new migrations
|
||||
continue-on-error: true
|
||||
run: npm --prefix server run typeorm:migrations:generate ./libs/infra/src/migrations/TestMigration
|
||||
run: npm --prefix server run typeorm:migrations:generate ./src/infra/migrations/TestMigration
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@v13.1
|
||||
id: verify-changed-files
|
||||
with:
|
||||
files: |
|
||||
server/libs/infra/src/migrations/
|
||||
server/src/infra/migrations/
|
||||
- name: Verify files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
|
||||
@@ -82,7 +82,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
| Public Sharing | No | Yes |
|
||||
| Archive and Favorites | Yes | Yes |
|
||||
| Global Map | No | Yes |
|
||||
| Partner Sharing | No | Yes |
|
||||
| Partner Sharing | Yes | Yes |
|
||||
| Facial recognition and clustering | No | Yes |
|
||||
|
||||
# Support the project
|
||||
|
||||
@@ -35,8 +35,7 @@ services:
|
||||
ports:
|
||||
- 3003:3003
|
||||
volumes:
|
||||
- ../machine-learning/src:/usr/src/app
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- ../machine-learning/app:/usr/src/app
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
- .env
|
||||
@@ -95,7 +94,7 @@ services:
|
||||
|
||||
typesense:
|
||||
container_name: immich_typesense
|
||||
image: typesense/typesense:0.24.0
|
||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||
environment:
|
||||
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
||||
- TYPESENSE_DATA_DIR=/data
|
||||
@@ -106,11 +105,11 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2
|
||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -25,12 +25,12 @@ services:
|
||||
- immich-test-network
|
||||
immich-redis-test:
|
||||
container_name: immich-redis-test
|
||||
image: redis:6.2
|
||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
||||
networks:
|
||||
- immich-test-network
|
||||
immich-database-test:
|
||||
container_name: immich-database-test
|
||||
image: postgres:14
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
env_file:
|
||||
- .env.test
|
||||
environment:
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: ["start-server.sh"]
|
||||
command: [ "start.sh", "immich" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
env_file:
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: ["start-microservices.sh"]
|
||||
command: [ "start.sh", "microservices" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
env_file:
|
||||
@@ -33,7 +33,6 @@ services:
|
||||
container_name: immich_machine_learning
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
- .env
|
||||
@@ -48,7 +47,7 @@ services:
|
||||
|
||||
typesense:
|
||||
container_name: immich_typesense
|
||||
image: typesense/typesense:0.24.0
|
||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||
environment:
|
||||
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
||||
- TYPESENSE_DATA_DIR=/data
|
||||
@@ -60,12 +59,12 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2
|
||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -13,9 +13,17 @@ docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/back
|
||||
```
|
||||
|
||||
```bash title='Restore'
|
||||
gunzip < /path/to/backup/dump.sql.gz | docker exec -i immich_postgres psql -U postgres -d immich
|
||||
docker-compose down -v # CAUTION! Deletes all Immich data to start from scratch.
|
||||
docker-compose pull # Update to latest version of Immich (if desired)
|
||||
docker-compose create # Create Docker containers for Immich apps without running them.
|
||||
docker start immich_postgres # Start Postgres server
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
gunzip < "/path/to/backup/dump.sql.gz" | docker exec -i immich_postgres psql -U postgres -d immich # Restore Backup
|
||||
docker-compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
|
||||
Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.).
|
||||
|
||||
The database dumps can also be automated (using [this image](https://github.com/prodrigestivill/docker-postgres-backup-local)) by editing the docker compose file to match the following:
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Database Migrations
|
||||
|
||||
After making any changes in the `server/libs/database/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
|
||||
After making any changes in the `server/src/infra/database/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
|
||||
|
||||
1. Run the command
|
||||
|
||||
```bash
|
||||
npm run typeorm:migrations:generate ./libs/infra/src/<migration-name>
|
||||
npm run typeorm:migrations:generate ./src/infra/<migration-name>
|
||||
```
|
||||
|
||||
2. Check if the migration file makes sense.
|
||||
3. Move the migration file to folder `server/libs/database/src/migrations` in your code editor.
|
||||
3. Move the migration file to folder `./src/infra/database/migrations` in your code editor.
|
||||
|
||||
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
||||
|
||||
@@ -30,4 +30,4 @@ The Open API client libraries need to be regenerated whenever there are changes
|
||||
|
||||
## Database Migrations
|
||||
|
||||
A database migration needs to be generated whenever there are changes to `server/libs/infra/src/entities`. See [Database Migration](/docs/developer/database-migrations.md) for more details.
|
||||
A database migration needs to be generated whenever there are changes to `server/src/infra/src/entities`. See [Database Migration](/docs/developer/database-migrations.md) for more details.
|
||||
|
||||
@@ -10,9 +10,9 @@ sidebar_position: 2
|
||||
|
||||
This environment includes the following services:
|
||||
|
||||
- Core server - `/server/apps/immich`
|
||||
- Core server - `/server/src/immich`
|
||||
- Machine learning - `/machine-learning`
|
||||
- Microservices - `/server/apps/microservicess`
|
||||
- Microservices - `/server/src/microservicess`
|
||||
- Web app - `/web`
|
||||
- Redis
|
||||
- PostgreSQL development database with exposed port `5432` so you can use any database client to acess it
|
||||
|
||||
@@ -76,10 +76,10 @@ If you are running the CLI container on the same machine as your Immich server,
|
||||
|
||||
1. Find the internal Docker network used by Immich via `docker network ls`.
|
||||
2. Adapt the above command to pass the `--network <immich_network>` argument to `docker run`, substituting `<immich_network>` with the result from step 1.
|
||||
3. Use `--server http://immich-server:3001/` for the upload command instead of the external address.
|
||||
3. Use `--server http://immich-server:3001` for the upload command instead of the external address.
|
||||
|
||||
```bash title="Upload to internal address"
|
||||
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://immich-server:3001/
|
||||
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://immich-server:3001
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
@@ -4,7 +4,7 @@ Immich can ingest XMP sidecars on file upload (via the CLI) as well as detect ne
|
||||
|
||||
<img src={require('./img/xmp-sidecars.png').default} title='XMP sidecars' />
|
||||
|
||||
XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the mdia file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary.
|
||||
XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the media file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary.
|
||||
|
||||
When importing files via the CLI bulk uploader, Immich will automatically detect XMP sidecar files as files that exist next to the original media file and have the exact same name with an additional `.xmp` file extension (i.e., `PXL_20230401_203352928.MP.jpg` and `PXL_20230401_203352928.MP.jpg.xmp`).
|
||||
|
||||
|
||||
776
docs/package-lock.json
generated
776
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@
|
||||
"@docusaurus/module-type-aliases": "2.1.0",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
FROM python:3.10 as builder
|
||||
FROM python:3.11.4-bullseye@sha256:5b401676aff858495a5c9c726c60b8b73fe52833e9e16eccdb59e93d52741727 as builder
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=true
|
||||
|
||||
RUN pip install --upgrade pip && pip install poetry
|
||||
RUN poetry config installer.max-workers 10 && \
|
||||
poetry config virtualenvs.create false
|
||||
RUN python -m venv /opt/venv
|
||||
RUN /opt/venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
RUN /opt/venv/bin/pip install transformers tqdm numpy scikit-learn scipy nltk sentencepiece fastapi Pillow uvicorn[standard]
|
||||
RUN /opt/venv/bin/pip install --no-deps sentence-transformers
|
||||
# Facial Recognition Stuff
|
||||
RUN /opt/venv/bin/pip install insightface onnxruntime
|
||||
ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
|
||||
|
||||
FROM python:3.10-slim
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
|
||||
ENV TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH"
|
||||
FROM python:3.11.4-slim-bullseye@sha256:91d194f58f50594cda71dcd2e8fdefd90e7ecc57d07823813b67c8521e565dcd
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=`pwd`
|
||||
|
||||
COPY . .
|
||||
ENV PYTHONPATH=`pwd`
|
||||
CMD ["python", "src/main.py"]
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
COPY app .
|
||||
ENTRYPOINT ["python", "main.py"]
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
|
||||
# Immich Machine Learning
|
||||
|
||||
- Object Detection
|
||||
- Image Classification
|
||||
- Image classification
|
||||
- CLIP embeddings
|
||||
- Facial recognition
|
||||
|
||||
# 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` will install everything you need in an isolated virtual environment.
|
||||
|
||||
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 to reflect any changes in dependencies.
|
||||
|
||||
84
machine-learning/app/cache.py
Normal file
84
machine-learning/app/cache.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from aiocache.plugins import TimingPlugin, BasePlugin
|
||||
from aiocache.backends.memory import SimpleMemoryCache
|
||||
from aiocache.lock import OptimisticLock
|
||||
from typing import Any
|
||||
from models import get_model
|
||||
|
||||
|
||||
class ModelCache:
|
||||
"""Fetches a model from an in-memory cache, instantiating it if it's missing."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ttl: int | None = None,
|
||||
revalidate: bool = False,
|
||||
timeout: int | None = None,
|
||||
profiling: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
ttl: Unloads model after this duration. Disabled if None. Defaults to None.
|
||||
revalidate: Resets TTL on cache hit. Useful to keep models in memory while active. Defaults to False.
|
||||
timeout: Maximum allowed time for model to load. Disabled if None. Defaults to None.
|
||||
profiling: Collects metrics for cache operations, adding slight overhead. Defaults to False.
|
||||
"""
|
||||
|
||||
self.ttl = ttl
|
||||
plugins = []
|
||||
|
||||
if revalidate:
|
||||
plugins.append(RevalidationPlugin())
|
||||
if profiling:
|
||||
plugins.append(TimingPlugin())
|
||||
|
||||
self.cache = SimpleMemoryCache(
|
||||
ttl=ttl, timeout=timeout, plugins=plugins, namespace=None
|
||||
)
|
||||
|
||||
async def get_cached_model(
|
||||
self, model_name: str, model_type: str, **model_kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
Args:
|
||||
model_name: Name of model in the model hub used for the task.
|
||||
model_type: Model type or task, which determines which model zoo is used.
|
||||
|
||||
Returns:
|
||||
model: The requested model.
|
||||
"""
|
||||
|
||||
key = self.cache.build_key(model_name, model_type)
|
||||
model = await self.cache.get(key)
|
||||
if model is None:
|
||||
async with OptimisticLock(self.cache, key) as lock:
|
||||
model = get_model(model_name, model_type, **model_kwargs)
|
||||
await lock.cas(model, ttl=self.ttl)
|
||||
return model
|
||||
|
||||
async def get_profiling(self) -> dict[str, float] | None:
|
||||
if not hasattr(self.cache, "profiling"):
|
||||
return None
|
||||
|
||||
return self.cache.profiling # type: ignore
|
||||
|
||||
|
||||
class RevalidationPlugin(BasePlugin):
|
||||
"""Revalidates cache item's TTL after cache hit."""
|
||||
|
||||
async def post_get(self, client, key, ret=None, namespace=None, **kwargs):
|
||||
if ret is None:
|
||||
return
|
||||
if namespace is not None:
|
||||
key = client.build_key(key, namespace)
|
||||
if key in client._handlers:
|
||||
await client.expire(key, client.ttl)
|
||||
|
||||
async def post_multi_get(self, client, keys, ret=None, namespace=None, **kwargs):
|
||||
if ret is None:
|
||||
return
|
||||
|
||||
for key, val in zip(keys, ret):
|
||||
if namespace is not None:
|
||||
key = client.build_key(key, namespace)
|
||||
if val is not None and key in client._handlers:
|
||||
await client.expire(key, client.ttl)
|
||||
22
machine-learning/app/config.py
Normal file
22
machine-learning/app/config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from pydantic import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
cache_folder: str = "/cache"
|
||||
classification_model: str = "microsoft/resnet-50"
|
||||
clip_image_model: str = "clip-ViT-B-32"
|
||||
clip_text_model: str = "clip-ViT-B-32"
|
||||
facial_recognition_model: str = "buffalo_l"
|
||||
min_tag_score: float = 0.9
|
||||
eager_startup: bool = True
|
||||
model_ttl: int = 300
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 3003
|
||||
workers: int = 1
|
||||
min_face_score: float = 0.7
|
||||
|
||||
class Config(BaseSettings.Config):
|
||||
env_prefix = 'MACHINE_LEARNING_'
|
||||
case_sensitive = False
|
||||
|
||||
|
||||
settings = Settings()
|
||||
133
machine-learning/app/main.py
Normal file
133
machine-learning/app/main.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import os
|
||||
import io
|
||||
from typing import Any
|
||||
|
||||
from cache import ModelCache
|
||||
from schemas import (
|
||||
EmbeddingResponse,
|
||||
FaceResponse,
|
||||
TagResponse,
|
||||
MessageResponse,
|
||||
TextModelRequest,
|
||||
TextResponse,
|
||||
)
|
||||
import uvicorn
|
||||
from PIL import Image
|
||||
from fastapi import FastAPI, HTTPException, Depends, Body
|
||||
from models import get_model, run_classification, run_facial_recognition
|
||||
from config import settings
|
||||
|
||||
_model_cache = None
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event() -> None:
|
||||
global _model_cache
|
||||
_model_cache = ModelCache(ttl=settings.model_ttl, revalidate=True)
|
||||
models = [
|
||||
(settings.classification_model, "image-classification"),
|
||||
(settings.clip_image_model, "clip"),
|
||||
(settings.clip_text_model, "clip"),
|
||||
(settings.facial_recognition_model, "facial-recognition"),
|
||||
]
|
||||
|
||||
# Get all models
|
||||
for model_name, model_type in models:
|
||||
if settings.eager_startup:
|
||||
await _model_cache.get_cached_model(model_name, model_type)
|
||||
else:
|
||||
get_model(model_name, model_type)
|
||||
|
||||
|
||||
def dep_model_cache():
|
||||
if _model_cache is None:
|
||||
raise HTTPException(status_code=500, detail="Unable to load model.")
|
||||
|
||||
def dep_input_image(image: bytes = Body(...)) -> Image:
|
||||
return Image.open(io.BytesIO(image))
|
||||
|
||||
@app.get("/", response_model=MessageResponse)
|
||||
async def root() -> dict[str, str]:
|
||||
return {"message": "Immich ML"}
|
||||
|
||||
|
||||
@app.get("/ping", response_model=TextResponse)
|
||||
def ping() -> str:
|
||||
return "pong"
|
||||
|
||||
|
||||
@app.post(
|
||||
"/image-classifier/tag-image",
|
||||
response_model=TagResponse,
|
||||
status_code=200,
|
||||
dependencies=[Depends(dep_model_cache)],
|
||||
)
|
||||
async def image_classification(
|
||||
image: Image = Depends(dep_input_image)
|
||||
) -> list[str]:
|
||||
try:
|
||||
model = await _model_cache.get_cached_model(
|
||||
settings.classification_model, "image-classification"
|
||||
)
|
||||
labels = run_classification(model, image, settings.min_tag_score)
|
||||
except Exception as ex:
|
||||
raise HTTPException(status_code=500, detail=str(ex))
|
||||
else:
|
||||
return labels
|
||||
|
||||
|
||||
@app.post(
|
||||
"/sentence-transformer/encode-image",
|
||||
response_model=EmbeddingResponse,
|
||||
status_code=200,
|
||||
dependencies=[Depends(dep_model_cache)],
|
||||
)
|
||||
async def clip_encode_image(
|
||||
image: Image = Depends(dep_input_image)
|
||||
) -> list[float]:
|
||||
model = await _model_cache.get_cached_model(settings.clip_image_model, "clip")
|
||||
embedding = model.encode(image).tolist()
|
||||
return embedding
|
||||
|
||||
|
||||
@app.post(
|
||||
"/sentence-transformer/encode-text",
|
||||
response_model=EmbeddingResponse,
|
||||
status_code=200,
|
||||
dependencies=[Depends(dep_model_cache)],
|
||||
)
|
||||
async def clip_encode_text(
|
||||
payload: TextModelRequest
|
||||
) -> list[float]:
|
||||
model = await _model_cache.get_cached_model(settings.clip_text_model, "clip")
|
||||
embedding = model.encode(payload.text).tolist()
|
||||
return embedding
|
||||
|
||||
|
||||
@app.post(
|
||||
"/facial-recognition/detect-faces",
|
||||
response_model=FaceResponse,
|
||||
status_code=200,
|
||||
dependencies=[Depends(dep_model_cache)],
|
||||
)
|
||||
async def facial_recognition(
|
||||
image: bytes = Body(...),
|
||||
) -> list[dict[str, Any]]:
|
||||
model = await _model_cache.get_cached_model(
|
||||
settings.facial_recognition_model, "facial-recognition"
|
||||
)
|
||||
faces = run_facial_recognition(model, image)
|
||||
return faces
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
is_dev = os.getenv("NODE_ENV") == "development"
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=is_dev,
|
||||
workers=settings.workers,
|
||||
)
|
||||
119
machine-learning/app/models.py
Normal file
119
machine-learning/app/models.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import torch
|
||||
from insightface.app import FaceAnalysis
|
||||
from pathlib import Path
|
||||
|
||||
from transformers import pipeline, Pipeline
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from typing import Any, BinaryIO
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from config import settings
|
||||
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
|
||||
def get_model(model_name: str, model_type: str, **model_kwargs):
|
||||
"""
|
||||
Instantiates the specified model.
|
||||
|
||||
Args:
|
||||
model_name: Name of model in the model hub used for the task.
|
||||
model_type: Model type or task, which determines which model zoo is used.
|
||||
`facial-recognition` uses Insightface, while all other models use the HF Model Hub.
|
||||
|
||||
Options:
|
||||
`image-classification`, `clip`,`facial-recognition`, `tokenizer`, `processor`
|
||||
|
||||
Returns:
|
||||
model: The requested model.
|
||||
"""
|
||||
|
||||
cache_dir = _get_cache_dir(model_name, model_type)
|
||||
match model_type:
|
||||
case "facial-recognition":
|
||||
model = _load_facial_recognition(
|
||||
model_name, cache_dir=cache_dir, **model_kwargs
|
||||
)
|
||||
case "clip":
|
||||
model = SentenceTransformer(
|
||||
model_name, cache_folder=cache_dir, **model_kwargs
|
||||
)
|
||||
case _:
|
||||
model = pipeline(
|
||||
model_type,
|
||||
model_name,
|
||||
model_kwargs={"cache_dir": cache_dir, **model_kwargs},
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def run_classification(
|
||||
model: Pipeline, image: Image, min_score: float | None = None
|
||||
):
|
||||
predictions: list[dict[str, Any]] = model(image) # type: ignore
|
||||
result = {
|
||||
tag
|
||||
for pred in predictions
|
||||
for tag in pred["label"].split(", ")
|
||||
if min_score is None or pred["score"] >= min_score
|
||||
}
|
||||
|
||||
return list(result)
|
||||
|
||||
|
||||
def run_facial_recognition(
|
||||
model: FaceAnalysis, image: bytes
|
||||
) -> list[dict[str, Any]]:
|
||||
file_bytes = np.frombuffer(image, dtype=np.uint8)
|
||||
img = cv.imdecode(file_bytes, cv.IMREAD_COLOR)
|
||||
height, width, _ = img.shape
|
||||
results = []
|
||||
faces = model.get(img)
|
||||
|
||||
for face in faces:
|
||||
x1, y1, x2, y2 = face.bbox
|
||||
|
||||
results.append(
|
||||
{
|
||||
"imageWidth": width,
|
||||
"imageHeight": height,
|
||||
"boundingBox": {
|
||||
"x1": round(x1),
|
||||
"y1": round(y1),
|
||||
"x2": round(x2),
|
||||
"y2": round(y2),
|
||||
},
|
||||
"score": face.det_score.item(),
|
||||
"embedding": face.normed_embedding.tolist(),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _load_facial_recognition(
|
||||
model_name: str,
|
||||
min_face_score: float | None = None,
|
||||
cache_dir: Path | str | None = None,
|
||||
**model_kwargs,
|
||||
):
|
||||
if cache_dir is None:
|
||||
cache_dir = _get_cache_dir(model_name, "facial-recognition")
|
||||
if isinstance(cache_dir, Path):
|
||||
cache_dir = cache_dir.as_posix()
|
||||
if min_face_score is None:
|
||||
min_face_score = settings.min_face_score
|
||||
|
||||
model = FaceAnalysis(
|
||||
name=model_name,
|
||||
root=cache_dir,
|
||||
allowed_modules=["detection", "recognition"],
|
||||
**model_kwargs,
|
||||
)
|
||||
model.prepare(ctx_id=0, det_thresh=min_face_score, det_size=(640, 640))
|
||||
return model
|
||||
|
||||
|
||||
def _get_cache_dir(model_name: str, model_type: str) -> Path:
|
||||
return Path(settings.cache_folder, device, model_type, model_name)
|
||||
56
machine-learning/app/schemas.py
Normal file
56
machine-learning/app/schemas.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def to_lower_camel(string: str) -> str:
|
||||
tokens = [
|
||||
token.capitalize() if i > 0 else token
|
||||
for i, token in enumerate(string.split("_"))
|
||||
]
|
||||
return "".join(tokens)
|
||||
|
||||
|
||||
class TextModelRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class TextResponse(BaseModel):
|
||||
__root__: str
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class TagResponse(BaseModel):
|
||||
__root__: list[str]
|
||||
|
||||
|
||||
class Embedding(BaseModel):
|
||||
__root__: list[float]
|
||||
|
||||
|
||||
class EmbeddingResponse(BaseModel):
|
||||
__root__: Embedding
|
||||
|
||||
|
||||
class BoundingBox(BaseModel):
|
||||
x1: int
|
||||
y1: int
|
||||
x2: int
|
||||
y2: int
|
||||
|
||||
|
||||
class Face(BaseModel):
|
||||
image_width: int
|
||||
image_height: int
|
||||
bounding_box: BoundingBox
|
||||
score: float
|
||||
embedding: Embedding
|
||||
|
||||
class Config:
|
||||
alias_generator = to_lower_camel
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class FaceResponse(BaseModel):
|
||||
__root__: list[Face]
|
||||
2472
machine-learning/poetry.lock
generated
Normal file
2472
machine-learning/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
machine-learning/pyproject.toml
Normal file
57
machine-learning/pyproject.toml
Normal file
@@ -0,0 +1,57 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.59.1"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
packages = [{include = "app"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
torch = [
|
||||
{markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=2.0.1", source = "pypi"},
|
||||
{markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.0.1", source = "pytorch-cpu"}
|
||||
]
|
||||
transformers = "^4.29.2"
|
||||
sentence-transformers = "^2.2.2"
|
||||
onnxruntime = "^1.15.0"
|
||||
insightface = "^0.7.3"
|
||||
opencv-python-headless = "^4.7.0.72"
|
||||
pillow = "^9.5.0"
|
||||
fastapi = "^0.95.2"
|
||||
uvicorn = {extras = ["standard"], version = "^0.22.0"}
|
||||
pydantic = "^1.10.8"
|
||||
aiocache = "^0.12.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = "^1.3.0"
|
||||
black = "^23.3.0"
|
||||
pytest = "^7.3.1"
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "pytorch-cpu"
|
||||
url = "https://download.pytorch.org/whl/cpu"
|
||||
priority = "explicit"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.flake8]
|
||||
max-line-length = 120
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
plugins = "pydantic.mypy"
|
||||
follow_imports = "silent"
|
||||
warn_redundant_casts = true
|
||||
disallow_any_generics = true
|
||||
check_untyped_defs = true
|
||||
no_implicit_reexport = true
|
||||
disallow_untyped_defs = true
|
||||
|
||||
[tool.pydantic-mypy]
|
||||
init_forbid_extra = true
|
||||
init_typed = true
|
||||
warn_required_dynamic_aliases = true
|
||||
warn_untyped_fields = true
|
||||
@@ -1,169 +0,0 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import cv2 as cv
|
||||
import uvicorn
|
||||
|
||||
from insightface.app import FaceAnalysis
|
||||
from transformers import pipeline
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from PIL import Image
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MlRequestBody(BaseModel):
|
||||
thumbnailPath: str
|
||||
|
||||
|
||||
class ClipRequestBody(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
classification_model = os.getenv(
|
||||
"MACHINE_LEARNING_CLASSIFICATION_MODEL", "microsoft/resnet-50"
|
||||
)
|
||||
clip_image_model = os.getenv("MACHINE_LEARNING_CLIP_IMAGE_MODEL", "clip-ViT-B-32")
|
||||
clip_text_model = os.getenv("MACHINE_LEARNING_CLIP_TEXT_MODEL", "clip-ViT-B-32")
|
||||
facial_recognition_model = os.getenv(
|
||||
"MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL", "buffalo_l"
|
||||
)
|
||||
|
||||
min_face_score = float(os.getenv("MACHINE_LEARNING_MIN_FACE_SCORE", 0.7))
|
||||
min_tag_score = float(os.getenv("MACHINE_LEARNING_MIN_TAG_SCORE", 0.9))
|
||||
eager_startup = (
|
||||
os.getenv("MACHINE_LEARNING_EAGER_STARTUP", "true") == "true"
|
||||
) # loads all models at startup
|
||||
|
||||
cache_folder = os.getenv("MACHINE_LEARNING_CACHE_FOLDER", "/cache")
|
||||
|
||||
_model_cache = {}
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
models = [
|
||||
(classification_model, "image-classification"),
|
||||
(clip_image_model, "clip"),
|
||||
(clip_text_model, "clip"),
|
||||
(facial_recognition_model, "facial-recognition"),
|
||||
]
|
||||
|
||||
# Get all models
|
||||
for model_name, model_type in models:
|
||||
if eager_startup:
|
||||
get_cached_model(model_name, model_type)
|
||||
else:
|
||||
_get_model(model_name, model_type)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Immich ML"}
|
||||
|
||||
|
||||
@app.get("/ping")
|
||||
def ping():
|
||||
return "pong"
|
||||
|
||||
|
||||
@app.post("/image-classifier/tag-image", status_code=200)
|
||||
def image_classification(payload: MlRequestBody):
|
||||
model = get_cached_model(classification_model, "image-classification")
|
||||
assetPath = payload.thumbnailPath
|
||||
return run_engine(model, assetPath)
|
||||
|
||||
|
||||
@app.post("/sentence-transformer/encode-image", status_code=200)
|
||||
def clip_encode_image(payload: MlRequestBody):
|
||||
model = get_cached_model(clip_image_model, "clip")
|
||||
assetPath = payload.thumbnailPath
|
||||
return model.encode(Image.open(assetPath)).tolist()
|
||||
|
||||
|
||||
@app.post("/sentence-transformer/encode-text", status_code=200)
|
||||
def clip_encode_text(payload: ClipRequestBody):
|
||||
model = get_cached_model(clip_text_model, "clip")
|
||||
text = payload.text
|
||||
return model.encode(text).tolist()
|
||||
|
||||
|
||||
@app.post("/facial-recognition/detect-faces", status_code=200)
|
||||
def facial_recognition(payload: MlRequestBody):
|
||||
model = get_cached_model(facial_recognition_model, "facial-recognition")
|
||||
assetPath = payload.thumbnailPath
|
||||
img = cv.imread(assetPath)
|
||||
height, width, _ = img.shape
|
||||
results = []
|
||||
faces = model.get(img)
|
||||
|
||||
for face in faces:
|
||||
if face.det_score < min_face_score:
|
||||
continue
|
||||
x1, y1, x2, y2 = face.bbox
|
||||
|
||||
results.append(
|
||||
{
|
||||
"imageWidth": width,
|
||||
"imageHeight": height,
|
||||
"boundingBox": {
|
||||
"x1": round(x1),
|
||||
"y1": round(y1),
|
||||
"x2": round(x2),
|
||||
"y2": round(y2),
|
||||
},
|
||||
"score": face.det_score.item(),
|
||||
"embedding": face.normed_embedding.tolist(),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def run_engine(engine, path):
|
||||
result = []
|
||||
predictions = engine(path)
|
||||
|
||||
for index, pred in enumerate(predictions):
|
||||
tags = pred["label"].split(", ")
|
||||
if pred["score"] > min_tag_score:
|
||||
result = [*result, *tags]
|
||||
|
||||
if len(result) > 1:
|
||||
result = list(set(result))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_cached_model(model, task):
|
||||
global _model_cache
|
||||
key = "|".join([model, str(task)])
|
||||
if key not in _model_cache:
|
||||
model = _get_model(model, task)
|
||||
_model_cache[key] = model
|
||||
|
||||
return _model_cache[key]
|
||||
|
||||
|
||||
def _get_model(model, task):
|
||||
match task:
|
||||
case "facial-recognition":
|
||||
model = FaceAnalysis(
|
||||
name=model,
|
||||
root=cache_folder,
|
||||
allowed_modules=["detection", "recognition"],
|
||||
)
|
||||
model.prepare(ctx_id=0, det_size=(640, 640))
|
||||
case "clip":
|
||||
model = SentenceTransformer(model, cache_folder=cache_folder)
|
||||
case _:
|
||||
model = pipeline(model=model, task=task)
|
||||
return model
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = os.getenv("MACHINE_LEARNING_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("MACHINE_LEARNING_PORT", 3003))
|
||||
is_dev = os.getenv("NODE_ENV") == "development"
|
||||
|
||||
uvicorn.run("main:app", host=host, port=port, reload=is_dev, workers=1)
|
||||
@@ -63,6 +63,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
||||
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
|
||||
npm --prefix server version $SERVER_PUMP
|
||||
npm --prefix server run api:generate
|
||||
poetry --directory machine-learning version $SERVER_PUMP
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
|
||||
|
||||
@@ -72,6 +72,11 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix '.debug'
|
||||
versionNameSuffix '-DEBUG'
|
||||
}
|
||||
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
@@ -84,6 +89,7 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
||||
implementation "com.google.guava:guava:$guava_version"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package app.alextran.immich
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.security.MessageDigest
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `BackgroundService`
|
||||
@@ -16,6 +21,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private var methodChannel: MethodChannel? = null
|
||||
private var context: Context? = null
|
||||
private val sha1: MessageDigest = MessageDigest.getInstance("SHA-1")
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||
@@ -70,9 +76,40 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
"isIgnoringBatteryOptimizations" -> {
|
||||
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
|
||||
}
|
||||
"digestFiles" -> {
|
||||
val args = call.arguments<ArrayList<String>>()!!
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val buf = ByteArray(BUFSIZE)
|
||||
val digest: MessageDigest = MessageDigest.getInstance("SHA-1")
|
||||
val hashes = arrayOfNulls<ByteArray>(args.size)
|
||||
for (i in args.indices) {
|
||||
val path = args[i]
|
||||
var len = 0
|
||||
try {
|
||||
val file = FileInputStream(path)
|
||||
try {
|
||||
while (true) {
|
||||
len = file.read(buf)
|
||||
if (len != BUFSIZE) break
|
||||
digest.update(buf)
|
||||
}
|
||||
} finally {
|
||||
file.close()
|
||||
}
|
||||
digest.update(buf, 0, len)
|
||||
hashes[i] = digest.digest()
|
||||
} catch (e: Exception) {
|
||||
// skip this file
|
||||
Log.w(TAG, "Failed to hash file ${args[i]}: $e")
|
||||
}
|
||||
}
|
||||
result.success(hashes.asList())
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
private const val BUFSIZE = 2*1024*1024;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.8.20'
|
||||
ext.kotlin_coroutines_version = '1.7.1'
|
||||
ext.work_version = '2.7.1'
|
||||
ext.concurrent_version = '1.1.0'
|
||||
ext.guava_version = '31.0.1-android'
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 83,
|
||||
"android.injected.version.name" => "1.60.0",
|
||||
"android.injected.version.code" => 85,
|
||||
"android.injected.version.name" => "1.62.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.60.0"
|
||||
version_number: "1.62.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -19,9 +19,11 @@ import 'package:immich_mobile/modules/settings/providers/notification_permission
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/android_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/ios_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
@@ -91,6 +93,7 @@ Future<Isar> loadDb() async {
|
||||
DuplicatedAssetSchema,
|
||||
LoggerMessageSchema,
|
||||
ETagSchema,
|
||||
Platform.isAndroid ? AndroidDeviceAssetSchema : IOSDeviceAssetSchema,
|
||||
],
|
||||
directory: dir.path,
|
||||
maxSizeMiB: 256,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
@@ -12,9 +13,13 @@ import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||
final ImageViewerService _imageViewerService;
|
||||
final ShareService _shareService;
|
||||
final AlbumService _albumService;
|
||||
|
||||
ImageViewerStateNotifier(this._imageViewerService, this._shareService)
|
||||
: super(
|
||||
ImageViewerStateNotifier(
|
||||
this._imageViewerService,
|
||||
this._shareService,
|
||||
this._albumService,
|
||||
) : super(
|
||||
ImageViewerPageState(
|
||||
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||
),
|
||||
@@ -34,6 +39,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||
toastType: ToastType.success,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
_albumService.refreshDeviceAlbums();
|
||||
} else {
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
|
||||
ImmichToast.show(
|
||||
@@ -66,5 +72,6 @@ final imageViewerStateProvider =
|
||||
((ref) => ImageViewerStateNotifier(
|
||||
ref.watch(imageViewerServiceProvider),
|
||||
ref.watch(shareServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
)),
|
||||
);
|
||||
|
||||
@@ -72,15 +72,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
if (!asset.isLocal)
|
||||
IconButton(
|
||||
onPressed: onDownloadPressed,
|
||||
icon: Icon(
|
||||
Icons.cloud_download_outlined,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
if (asset.storage == AssetState.merged)
|
||||
if (asset.storage == AssetState.remote)
|
||||
IconButton(
|
||||
onPressed: onDownloadPressed,
|
||||
icon: Icon(
|
||||
|
||||
@@ -276,28 +276,33 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
(showAppBar.value && !isZoomed.value)) &&
|
||||
!isPlayingVideo.value;
|
||||
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: TopControlAppBar(
|
||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||
asset: asset(),
|
||||
isFavorite: asset().isFavorite,
|
||||
onMoreInfoPressed: showInfo,
|
||||
onFavorite: asset().isRemote ? () => toggleFavorite(asset()) : null,
|
||||
onDownloadPressed: asset().storage == AssetState.local
|
||||
? null
|
||||
: () =>
|
||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||
asset(),
|
||||
context,
|
||||
),
|
||||
onToggleMotionVideo: (() {
|
||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||
}),
|
||||
onAddToAlbumPressed: () => addToAlbum(asset()),
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: TopControlAppBar(
|
||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||
asset: asset(),
|
||||
isFavorite: asset().isFavorite,
|
||||
onMoreInfoPressed: showInfo,
|
||||
onFavorite:
|
||||
asset().isRemote ? () => toggleFavorite(asset()) : null,
|
||||
onDownloadPressed: asset().isLocal
|
||||
? null
|
||||
: () => ref
|
||||
.watch(imageViewerStateProvider.notifier)
|
||||
.downloadAsset(
|
||||
asset(),
|
||||
context,
|
||||
),
|
||||
onToggleMotionVideo: (() {
|
||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||
}),
|
||||
onAddToAlbumPressed: () => addToAlbum(asset()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -307,53 +312,57 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final show = (showAppBar.value || // onTap has the final say
|
||||
(showAppBar.value && !isZoomed.value)) &&
|
||||
!isPlayingVideo.value;
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
child: BottomNavigationBar(
|
||||
backgroundColor: Colors.black.withOpacity(0.4),
|
||||
unselectedIconTheme: const IconThemeData(color: Colors.white),
|
||||
selectedIconTheme: const IconThemeData(color: Colors.white),
|
||||
unselectedLabelStyle: const TextStyle(color: Colors.black),
|
||||
selectedLabelStyle: const TextStyle(color: Colors.black),
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.ios_share_rounded),
|
||||
label: 'control_bottom_app_bar_share'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_share'.tr(),
|
||||
),
|
||||
asset().isArchived
|
||||
? BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.unarchive_rounded),
|
||||
label: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
)
|
||||
: BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.archive_outlined),
|
||||
label: 'control_bottom_app_bar_archive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_archive'.tr(),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: 'control_bottom_app_bar_delete'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_delete'.tr(),
|
||||
),
|
||||
],
|
||||
onTap: (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
shareAsset();
|
||||
break;
|
||||
case 1:
|
||||
handleArchive(asset());
|
||||
break;
|
||||
case 2:
|
||||
handleDelete(asset());
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
child: BottomNavigationBar(
|
||||
backgroundColor: Colors.black.withOpacity(0.4),
|
||||
unselectedIconTheme: const IconThemeData(color: Colors.white),
|
||||
selectedIconTheme: const IconThemeData(color: Colors.white),
|
||||
unselectedLabelStyle: const TextStyle(color: Colors.black),
|
||||
selectedLabelStyle: const TextStyle(color: Colors.black),
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.ios_share_rounded),
|
||||
label: 'control_bottom_app_bar_share'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_share'.tr(),
|
||||
),
|
||||
asset().isArchived
|
||||
? BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.unarchive_rounded),
|
||||
label: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
|
||||
)
|
||||
: BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.archive_outlined),
|
||||
label: 'control_bottom_app_bar_archive'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_archive'.tr(),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: 'control_bottom_app_bar_delete'.tr(),
|
||||
tooltip: 'control_bottom_app_bar_delete'.tr(),
|
||||
),
|
||||
],
|
||||
onTap: (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
shareAsset();
|
||||
break;
|
||||
case 1:
|
||||
handleArchive(asset());
|
||||
break;
|
||||
case 2:
|
||||
handleDelete(asset());
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,17 @@ class BackgroundService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> digestFile(String path) {
|
||||
return _foregroundChannel.invokeMethod<Uint8List>("digestFile", [path]);
|
||||
}
|
||||
|
||||
Future<List<Uint8List?>?> digestFiles(List<String> paths) {
|
||||
return _foregroundChannel.invokeListMethod<Uint8List?>(
|
||||
"digestFiles",
|
||||
paths,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates the notification shown by the background service
|
||||
Future<bool?> _updateNotification({
|
||||
String? title,
|
||||
|
||||
@@ -47,11 +47,11 @@ class HomePage extends HookConsumerWidget {
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.watch(websocketProvider.notifier).connect();
|
||||
ref.watch(assetProvider.notifier).getAllAsset();
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||
|
||||
selectionEnabledHook.addListener(() {
|
||||
multiselectEnabled.state = selectionEnabledHook.value;
|
||||
@@ -144,7 +144,7 @@ class HomePage extends HookConsumerWidget {
|
||||
);
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
await ref
|
||||
.watch(assetProvider.notifier)
|
||||
.read(assetProvider.notifier)
|
||||
.toggleArchive(remoteAssets, true);
|
||||
|
||||
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
|
||||
@@ -163,7 +163,7 @@ class HomePage extends HookConsumerWidget {
|
||||
void onDelete() async {
|
||||
processing.value = true;
|
||||
try {
|
||||
await ref.watch(assetProvider.notifier).deleteAssets(selection.value);
|
||||
await ref.read(assetProvider.notifier).deleteAssets(selection.value);
|
||||
selectionEnabledHook.value = false;
|
||||
} finally {
|
||||
processing.value = false;
|
||||
|
||||
@@ -166,23 +166,10 @@ extension AssetsHelper on IsarCollection<Album> {
|
||||
}
|
||||
}
|
||||
|
||||
extension AssetPathEntityHelper on AssetPathEntity {
|
||||
Future<List<Asset>> getAssets({
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
Set<String>? excludedAssets,
|
||||
}) async {
|
||||
final assetEntities = await getAssetListRange(start: start, end: end);
|
||||
if (excludedAssets != null) {
|
||||
return assetEntities
|
||||
.where((e) => !excludedAssets.contains(e.id))
|
||||
.map(Asset.local)
|
||||
.toList();
|
||||
}
|
||||
return assetEntities.map(Asset.local).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
||||
}
|
||||
|
||||
extension AssetPathEntityHelper on AssetPathEntity {
|
||||
String get eTagKeyAssetCount => "device-album-$id-asset-count";
|
||||
}
|
||||
|
||||
10
mobile/lib/shared/models/android_device_asset.dart
Normal file
10
mobile/lib/shared/models/android_device_asset.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'android_device_asset.g.dart';
|
||||
|
||||
@Collection()
|
||||
class AndroidDeviceAsset extends DeviceAsset {
|
||||
AndroidDeviceAsset({required this.id, required super.hash});
|
||||
Id id;
|
||||
}
|
||||
493
mobile/lib/shared/models/android_device_asset.g.dart
Normal file
493
mobile/lib/shared/models/android_device_asset.g.dart
Normal file
@@ -0,0 +1,493 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'android_device_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetAndroidDeviceAssetCollection on Isar {
|
||||
IsarCollection<AndroidDeviceAsset> get androidDeviceAssets =>
|
||||
this.collection();
|
||||
}
|
||||
|
||||
const AndroidDeviceAssetSchema = CollectionSchema(
|
||||
name: r'AndroidDeviceAsset',
|
||||
id: -6758387181232899335,
|
||||
properties: {
|
||||
r'hash': PropertySchema(
|
||||
id: 0,
|
||||
name: r'hash',
|
||||
type: IsarType.byteList,
|
||||
)
|
||||
},
|
||||
estimateSize: _androidDeviceAssetEstimateSize,
|
||||
serialize: _androidDeviceAssetSerialize,
|
||||
deserialize: _androidDeviceAssetDeserialize,
|
||||
deserializeProp: _androidDeviceAssetDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {
|
||||
r'hash': IndexSchema(
|
||||
id: -7973251393006690288,
|
||||
name: r'hash',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'hash',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _androidDeviceAssetGetId,
|
||||
getLinks: _androidDeviceAssetGetLinks,
|
||||
attach: _androidDeviceAssetAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _androidDeviceAssetEstimateSize(
|
||||
AndroidDeviceAsset object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.hash.length;
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _androidDeviceAssetSerialize(
|
||||
AndroidDeviceAsset object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeByteList(offsets[0], object.hash);
|
||||
}
|
||||
|
||||
AndroidDeviceAsset _androidDeviceAssetDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = AndroidDeviceAsset(
|
||||
hash: reader.readByteList(offsets[0]) ?? [],
|
||||
id: id,
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _androidDeviceAssetDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _androidDeviceAssetGetId(AndroidDeviceAsset object) {
|
||||
return object.id;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _androidDeviceAssetGetLinks(
|
||||
AndroidDeviceAsset object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _androidDeviceAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, AndroidDeviceAsset object) {
|
||||
object.id = id;
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhereSort
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhere> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhere
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhereClause> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: id,
|
||||
upper: id,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idNotEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idGreaterThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idLessThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idBetween(
|
||||
Id lowerId,
|
||||
Id upperId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerId,
|
||||
includeLower: includeLower,
|
||||
upper: upperId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
hashEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'hash',
|
||||
value: [hash],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
hashNotEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryFilter
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'hash',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idEqualTo(Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryObject
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension AndroidDeviceAssetQueryLinks
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension AndroidDeviceAssetQuerySortBy
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortBy> {}
|
||||
|
||||
extension AndroidDeviceAssetQuerySortThenBy
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortThenBy> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
|
||||
thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
|
||||
thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhereDistinct
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct>
|
||||
distinctByHash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryProperty
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QQueryProperty> {
|
||||
QueryBuilder<AndroidDeviceAsset, int, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, List<int>, QQueryOperations> hashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
@@ -14,7 +16,7 @@ part 'asset.g.dart';
|
||||
class Asset {
|
||||
Asset.remote(AssetResponseDto remote)
|
||||
: remoteId = remote.id,
|
||||
isLocal = false,
|
||||
checksum = remote.checksum,
|
||||
fileCreatedAt = remote.fileCreatedAt,
|
||||
fileModifiedAt = remote.fileModifiedAt,
|
||||
updatedAt = remote.updatedAt,
|
||||
@@ -24,23 +26,20 @@ class Asset {
|
||||
height = remote.exifInfo?.exifImageHeight?.toInt(),
|
||||
width = remote.exifInfo?.exifImageWidth?.toInt(),
|
||||
livePhotoVideoId = remote.livePhotoVideoId,
|
||||
localId = remote.deviceAssetId,
|
||||
deviceId = fastHash(remote.deviceId),
|
||||
ownerId = fastHash(remote.ownerId),
|
||||
exifInfo =
|
||||
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
||||
isFavorite = remote.isFavorite,
|
||||
isArchived = remote.isArchived;
|
||||
|
||||
Asset.local(AssetEntity local)
|
||||
Asset.local(AssetEntity local, List<int> hash)
|
||||
: localId = local.id,
|
||||
isLocal = true,
|
||||
checksum = base64.encode(hash),
|
||||
durationInSeconds = local.duration,
|
||||
type = AssetType.values[local.typeInt],
|
||||
height = local.height,
|
||||
width = local.width,
|
||||
fileName = local.title!,
|
||||
deviceId = Store.get(StoreKey.deviceIdHash),
|
||||
ownerId = Store.get(StoreKey.currentUser).isarId,
|
||||
fileModifiedAt = local.modifiedDateTime,
|
||||
updatedAt = local.modifiedDateTime,
|
||||
@@ -53,13 +52,15 @@ class Asset {
|
||||
if (local.latitude != null) {
|
||||
exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
|
||||
}
|
||||
_local = local;
|
||||
assert(hash.length == 20, "invalid SHA1 hash");
|
||||
}
|
||||
|
||||
Asset({
|
||||
this.id = Isar.autoIncrement,
|
||||
required this.checksum,
|
||||
this.remoteId,
|
||||
required this.localId,
|
||||
required this.deviceId,
|
||||
required this.ownerId,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
@@ -72,7 +73,6 @@ class Asset {
|
||||
this.livePhotoVideoId,
|
||||
this.exifInfo,
|
||||
required this.isFavorite,
|
||||
required this.isLocal,
|
||||
required this.isArchived,
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ class Asset {
|
||||
AssetEntity? get local {
|
||||
if (isLocal && _local == null) {
|
||||
_local = AssetEntity(
|
||||
id: localId,
|
||||
id: localId!,
|
||||
typeInt: isImage ? 1 : 2,
|
||||
width: width ?? 0,
|
||||
height: height ?? 0,
|
||||
@@ -98,18 +98,21 @@ class Asset {
|
||||
|
||||
Id id = Isar.autoIncrement;
|
||||
|
||||
/// stores the raw SHA1 bytes as a base64 String
|
||||
/// because Isar cannot sort lists of byte arrays
|
||||
@Index(
|
||||
unique: true,
|
||||
replace: false,
|
||||
type: IndexType.hash,
|
||||
composite: [CompositeIndex("ownerId")],
|
||||
)
|
||||
String checksum;
|
||||
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
|
||||
@Index(
|
||||
unique: false,
|
||||
replace: false,
|
||||
type: IndexType.hash,
|
||||
composite: [CompositeIndex('deviceId')],
|
||||
)
|
||||
String localId;
|
||||
|
||||
int deviceId;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? localId;
|
||||
|
||||
int ownerId;
|
||||
|
||||
@@ -134,14 +137,15 @@ class Asset {
|
||||
|
||||
bool isFavorite;
|
||||
|
||||
/// `true` if this [Asset] is present on the device
|
||||
bool isLocal;
|
||||
|
||||
bool isArchived;
|
||||
|
||||
@ignore
|
||||
ExifInfo? exifInfo;
|
||||
|
||||
/// `true` if this [Asset] is present on the device
|
||||
@ignore
|
||||
bool get isLocal => localId != null;
|
||||
|
||||
@ignore
|
||||
bool get isInDb => id != Isar.autoIncrement;
|
||||
|
||||
@@ -175,9 +179,9 @@ class Asset {
|
||||
bool operator ==(other) {
|
||||
if (other is! Asset) return false;
|
||||
return id == other.id &&
|
||||
checksum == other.checksum &&
|
||||
remoteId == other.remoteId &&
|
||||
localId == other.localId &&
|
||||
deviceId == other.deviceId &&
|
||||
ownerId == other.ownerId &&
|
||||
fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) &&
|
||||
fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) &&
|
||||
@@ -197,9 +201,9 @@ class Asset {
|
||||
@ignore
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
checksum.hashCode ^
|
||||
remoteId.hashCode ^
|
||||
localId.hashCode ^
|
||||
deviceId.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
fileCreatedAt.hashCode ^
|
||||
fileModifiedAt.hashCode ^
|
||||
@@ -217,8 +221,7 @@ class Asset {
|
||||
/// Returns `true` if this [Asset] can updated with values from parameter [a]
|
||||
bool canUpdate(Asset a) {
|
||||
assert(isInDb);
|
||||
assert(localId == a.localId);
|
||||
assert(deviceId == a.deviceId);
|
||||
assert(checksum == a.checksum);
|
||||
assert(a.storage != AssetState.merged);
|
||||
return a.updatedAt.isAfter(updatedAt) ||
|
||||
a.isRemote && !isRemote ||
|
||||
@@ -239,11 +242,18 @@ class Asset {
|
||||
if (a.isRemote) {
|
||||
return a._copyWith(
|
||||
id: id,
|
||||
isLocal: isLocal,
|
||||
localId: localId,
|
||||
width: a.width ?? width,
|
||||
height: a.height ?? height,
|
||||
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
||||
);
|
||||
} else if (isRemote) {
|
||||
return _copyWith(
|
||||
localId: localId ?? a.localId,
|
||||
width: width ?? a.width,
|
||||
height: height ?? a.height,
|
||||
exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
|
||||
);
|
||||
} else {
|
||||
return a._copyWith(
|
||||
id: id,
|
||||
@@ -270,7 +280,7 @@ class Asset {
|
||||
} else {
|
||||
// add only missing values (and set isLocal to true)
|
||||
return _copyWith(
|
||||
isLocal: true,
|
||||
localId: localId ?? a.localId,
|
||||
width: width ?? a.width,
|
||||
height: height ?? a.height,
|
||||
exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
|
||||
@@ -281,9 +291,9 @@ class Asset {
|
||||
|
||||
Asset _copyWith({
|
||||
Id? id,
|
||||
String? checksum,
|
||||
String? remoteId,
|
||||
String? localId,
|
||||
int? deviceId,
|
||||
int? ownerId,
|
||||
DateTime? fileCreatedAt,
|
||||
DateTime? fileModifiedAt,
|
||||
@@ -295,15 +305,14 @@ class Asset {
|
||||
String? fileName,
|
||||
String? livePhotoVideoId,
|
||||
bool? isFavorite,
|
||||
bool? isLocal,
|
||||
bool? isArchived,
|
||||
ExifInfo? exifInfo,
|
||||
}) =>
|
||||
Asset(
|
||||
id: id ?? this.id,
|
||||
checksum: checksum ?? this.checksum,
|
||||
remoteId: remoteId ?? this.remoteId,
|
||||
localId: localId ?? this.localId,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
|
||||
fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt,
|
||||
@@ -315,7 +324,6 @@ class Asset {
|
||||
fileName: fileName ?? this.fileName,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isLocal: isLocal ?? this.isLocal,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
exifInfo: exifInfo ?? this.exifInfo,
|
||||
);
|
||||
@@ -328,39 +336,36 @@ class Asset {
|
||||
}
|
||||
}
|
||||
|
||||
/// compares assets by [ownerId], [deviceId], [localId]
|
||||
static int compareByOwnerDeviceLocalId(Asset a, Asset b) {
|
||||
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
|
||||
if (ownerIdOrder != 0) {
|
||||
return ownerIdOrder;
|
||||
}
|
||||
final int deviceIdOrder = a.deviceId.compareTo(b.deviceId);
|
||||
if (deviceIdOrder != 0) {
|
||||
return deviceIdOrder;
|
||||
}
|
||||
final int localIdOrder = a.localId.compareTo(b.localId);
|
||||
return localIdOrder;
|
||||
}
|
||||
|
||||
/// compares assets by [ownerId], [deviceId], [localId], [fileModifiedAt]
|
||||
static int compareByOwnerDeviceLocalIdModified(Asset a, Asset b) {
|
||||
final int order = compareByOwnerDeviceLocalId(a, b);
|
||||
return order != 0 ? order : a.fileModifiedAt.compareTo(b.fileModifiedAt);
|
||||
}
|
||||
|
||||
static int compareById(Asset a, Asset b) => a.id.compareTo(b.id);
|
||||
|
||||
static int compareByLocalId(Asset a, Asset b) =>
|
||||
a.localId.compareTo(b.localId);
|
||||
static int compareByChecksum(Asset a, Asset b) =>
|
||||
a.checksum.compareTo(b.checksum);
|
||||
|
||||
static int compareByOwnerChecksum(Asset a, Asset b) {
|
||||
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
|
||||
if (ownerIdOrder != 0) return ownerIdOrder;
|
||||
return compareByChecksum(a, b);
|
||||
}
|
||||
|
||||
static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) {
|
||||
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
|
||||
if (ownerIdOrder != 0) return ownerIdOrder;
|
||||
final int checksumOrder = compareByChecksum(a, b);
|
||||
if (checksumOrder != 0) return checksumOrder;
|
||||
final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt);
|
||||
if (createdOrder != 0) return createdOrder;
|
||||
return a.fileModifiedAt.compareTo(b.fileModifiedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return """
|
||||
{
|
||||
"id": ${id == Isar.autoIncrement ? '"N/A"' : id},
|
||||
"remoteId": "${remoteId ?? "N/A"}",
|
||||
"localId": "$localId",
|
||||
"deviceId": "$deviceId",
|
||||
"ownerId": "$ownerId",
|
||||
"localId": "${localId ?? "N/A"}",
|
||||
"checksum": "$checksum",
|
||||
"ownerId": $ownerId,
|
||||
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
|
||||
"fileCreatedAt": "$fileCreatedAt",
|
||||
"fileModifiedAt": "$fileModifiedAt",
|
||||
@@ -369,9 +374,8 @@ class Asset {
|
||||
"type": "$type",
|
||||
"fileName": "$fileName",
|
||||
"isFavorite": $isFavorite,
|
||||
"isLocal": $isLocal,
|
||||
"isRemote: $isRemote,
|
||||
"storage": $storage,
|
||||
"storage": "$storage",
|
||||
"width": ${width ?? "N/A"},
|
||||
"height": ${height ?? "N/A"},
|
||||
"isArchived": $isArchived
|
||||
@@ -424,10 +428,6 @@ extension AssetsHelper on IsarCollection<Asset> {
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> _remote(Iterable<String> ids) =>
|
||||
where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e));
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> _local(Iterable<String> ids) {
|
||||
return where().anyOf(
|
||||
ids,
|
||||
(q, String e) =>
|
||||
q.localIdDeviceIdEqualTo(e, Store.get(StoreKey.deviceIdHash)),
|
||||
);
|
||||
return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ const AssetSchema = CollectionSchema(
|
||||
name: r'Asset',
|
||||
id: -2933289051367723566,
|
||||
properties: {
|
||||
r'deviceId': PropertySchema(
|
||||
r'checksum': PropertySchema(
|
||||
id: 0,
|
||||
name: r'deviceId',
|
||||
type: IsarType.long,
|
||||
name: r'checksum',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'durationInSeconds': PropertySchema(
|
||||
id: 1,
|
||||
@@ -57,44 +57,39 @@ const AssetSchema = CollectionSchema(
|
||||
name: r'isFavorite',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isLocal': PropertySchema(
|
||||
id: 8,
|
||||
name: r'isLocal',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'livePhotoVideoId': PropertySchema(
|
||||
id: 9,
|
||||
id: 8,
|
||||
name: r'livePhotoVideoId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'localId': PropertySchema(
|
||||
id: 10,
|
||||
id: 9,
|
||||
name: r'localId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'ownerId': PropertySchema(
|
||||
id: 11,
|
||||
id: 10,
|
||||
name: r'ownerId',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'remoteId': PropertySchema(
|
||||
id: 12,
|
||||
id: 11,
|
||||
name: r'remoteId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'type': PropertySchema(
|
||||
id: 13,
|
||||
id: 12,
|
||||
name: r'type',
|
||||
type: IsarType.byte,
|
||||
enumMap: _AssettypeEnumValueMap,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 14,
|
||||
id: 13,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'width': PropertySchema(
|
||||
id: 15,
|
||||
id: 14,
|
||||
name: r'width',
|
||||
type: IsarType.int,
|
||||
)
|
||||
@@ -105,6 +100,24 @@ const AssetSchema = CollectionSchema(
|
||||
deserializeProp: _assetDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {
|
||||
r'checksum_ownerId': IndexSchema(
|
||||
id: 5611361749756160119,
|
||||
name: r'checksum_ownerId',
|
||||
unique: true,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'checksum',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: true,
|
||||
),
|
||||
IndexPropertySchema(
|
||||
name: r'ownerId',
|
||||
type: IndexType.value,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
),
|
||||
r'remoteId': IndexSchema(
|
||||
id: 6301175856541681032,
|
||||
name: r'remoteId',
|
||||
@@ -118,9 +131,9 @@ const AssetSchema = CollectionSchema(
|
||||
)
|
||||
],
|
||||
),
|
||||
r'localId_deviceId': IndexSchema(
|
||||
id: 7649417350086526165,
|
||||
name: r'localId_deviceId',
|
||||
r'localId': IndexSchema(
|
||||
id: 1199848425898359622,
|
||||
name: r'localId',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
@@ -128,11 +141,6 @@ const AssetSchema = CollectionSchema(
|
||||
name: r'localId',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: true,
|
||||
),
|
||||
IndexPropertySchema(
|
||||
name: r'deviceId',
|
||||
type: IndexType.value,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -151,6 +159,7 @@ int _assetEstimateSize(
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.checksum.length * 3;
|
||||
bytesCount += 3 + object.fileName.length * 3;
|
||||
{
|
||||
final value = object.livePhotoVideoId;
|
||||
@@ -158,7 +167,12 @@ int _assetEstimateSize(
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
bytesCount += 3 + object.localId.length * 3;
|
||||
{
|
||||
final value = object.localId;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.remoteId;
|
||||
if (value != null) {
|
||||
@@ -174,7 +188,7 @@ void _assetSerialize(
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeLong(offsets[0], object.deviceId);
|
||||
writer.writeString(offsets[0], object.checksum);
|
||||
writer.writeLong(offsets[1], object.durationInSeconds);
|
||||
writer.writeDateTime(offsets[2], object.fileCreatedAt);
|
||||
writer.writeDateTime(offsets[3], object.fileModifiedAt);
|
||||
@@ -182,14 +196,13 @@ void _assetSerialize(
|
||||
writer.writeInt(offsets[5], object.height);
|
||||
writer.writeBool(offsets[6], object.isArchived);
|
||||
writer.writeBool(offsets[7], object.isFavorite);
|
||||
writer.writeBool(offsets[8], object.isLocal);
|
||||
writer.writeString(offsets[9], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[10], object.localId);
|
||||
writer.writeLong(offsets[11], object.ownerId);
|
||||
writer.writeString(offsets[12], object.remoteId);
|
||||
writer.writeByte(offsets[13], object.type.index);
|
||||
writer.writeDateTime(offsets[14], object.updatedAt);
|
||||
writer.writeInt(offsets[15], object.width);
|
||||
writer.writeString(offsets[8], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[9], object.localId);
|
||||
writer.writeLong(offsets[10], object.ownerId);
|
||||
writer.writeString(offsets[11], object.remoteId);
|
||||
writer.writeByte(offsets[12], object.type.index);
|
||||
writer.writeDateTime(offsets[13], object.updatedAt);
|
||||
writer.writeInt(offsets[14], object.width);
|
||||
}
|
||||
|
||||
Asset _assetDeserialize(
|
||||
@@ -199,7 +212,7 @@ Asset _assetDeserialize(
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = Asset(
|
||||
deviceId: reader.readLong(offsets[0]),
|
||||
checksum: reader.readString(offsets[0]),
|
||||
durationInSeconds: reader.readLong(offsets[1]),
|
||||
fileCreatedAt: reader.readDateTime(offsets[2]),
|
||||
fileModifiedAt: reader.readDateTime(offsets[3]),
|
||||
@@ -208,15 +221,14 @@ Asset _assetDeserialize(
|
||||
id: id,
|
||||
isArchived: reader.readBool(offsets[6]),
|
||||
isFavorite: reader.readBool(offsets[7]),
|
||||
isLocal: reader.readBool(offsets[8]),
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
|
||||
localId: reader.readString(offsets[10]),
|
||||
ownerId: reader.readLong(offsets[11]),
|
||||
remoteId: reader.readStringOrNull(offsets[12]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[8]),
|
||||
localId: reader.readStringOrNull(offsets[9]),
|
||||
ownerId: reader.readLong(offsets[10]),
|
||||
remoteId: reader.readStringOrNull(offsets[11]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[12])] ??
|
||||
AssetType.other,
|
||||
updatedAt: reader.readDateTime(offsets[14]),
|
||||
width: reader.readIntOrNull(offsets[15]),
|
||||
updatedAt: reader.readDateTime(offsets[13]),
|
||||
width: reader.readIntOrNull(offsets[14]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
@@ -229,7 +241,7 @@ P _assetDeserializeProp<P>(
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readLong(offset)) as P;
|
||||
return (reader.readString(offset)) as P;
|
||||
case 1:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 2:
|
||||
@@ -245,21 +257,19 @@ P _assetDeserializeProp<P>(
|
||||
case 7:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 8:
|
||||
return (reader.readBool(offset)) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 10:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 12:
|
||||
case 11:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 13:
|
||||
case 12:
|
||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AssetType.other) as P;
|
||||
case 14:
|
||||
case 13:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 15:
|
||||
case 14:
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
@@ -291,6 +301,94 @@ void _assetAttach(IsarCollection<dynamic> col, Id id, Asset object) {
|
||||
object.id = id;
|
||||
}
|
||||
|
||||
extension AssetByIndex on IsarCollection<Asset> {
|
||||
Future<Asset?> getByChecksumOwnerId(String checksum, int ownerId) {
|
||||
return getByIndex(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
Asset? getByChecksumOwnerIdSync(String checksum, int ownerId) {
|
||||
return getByIndexSync(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
Future<bool> deleteByChecksumOwnerId(String checksum, int ownerId) {
|
||||
return deleteByIndex(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
bool deleteByChecksumOwnerIdSync(String checksum, int ownerId) {
|
||||
return deleteByIndexSync(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
Future<List<Asset?>> getAllByChecksumOwnerId(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return getAllByIndex(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
List<Asset?> getAllByChecksumOwnerIdSync(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return getAllByIndexSync(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
Future<int> deleteAllByChecksumOwnerId(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return deleteAllByIndex(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
int deleteAllByChecksumOwnerIdSync(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return deleteAllByIndexSync(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
Future<Id> putByChecksumOwnerId(Asset object) {
|
||||
return putByIndex(r'checksum_ownerId', object);
|
||||
}
|
||||
|
||||
Id putByChecksumOwnerIdSync(Asset object, {bool saveLinks = true}) {
|
||||
return putByIndexSync(r'checksum_ownerId', object, saveLinks: saveLinks);
|
||||
}
|
||||
|
||||
Future<List<Id>> putAllByChecksumOwnerId(List<Asset> objects) {
|
||||
return putAllByIndex(r'checksum_ownerId', objects);
|
||||
}
|
||||
|
||||
List<Id> putAllByChecksumOwnerIdSync(List<Asset> objects,
|
||||
{bool saveLinks = true}) {
|
||||
return putAllByIndexSync(r'checksum_ownerId', objects,
|
||||
saveLinks: saveLinks);
|
||||
}
|
||||
}
|
||||
|
||||
extension AssetQueryWhereSort on QueryBuilder<Asset, Asset, QWhere> {
|
||||
QueryBuilder<Asset, Asset, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -365,6 +463,145 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumEqualToAnyOwnerId(
|
||||
String checksum) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'checksum_ownerId',
|
||||
value: [checksum],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumNotEqualToAnyOwnerId(
|
||||
String checksum) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [],
|
||||
upper: [checksum],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [],
|
||||
upper: [checksum],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumOwnerIdEqualTo(
|
||||
String checksum, int ownerId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'checksum_ownerId',
|
||||
value: [checksum, ownerId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
checksumEqualToOwnerIdNotEqualTo(String checksum, int ownerId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
upper: [checksum, ownerId],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, ownerId],
|
||||
includeLower: false,
|
||||
upper: [checksum],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, ownerId],
|
||||
includeLower: false,
|
||||
upper: [checksum],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
upper: [checksum, ownerId],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
checksumEqualToOwnerIdGreaterThan(
|
||||
String checksum,
|
||||
int ownerId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, ownerId],
|
||||
includeLower: include,
|
||||
upper: [checksum],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumEqualToOwnerIdLessThan(
|
||||
String checksum,
|
||||
int ownerId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
upper: [checksum, ownerId],
|
||||
includeUpper: include,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumEqualToOwnerIdBetween(
|
||||
String checksum,
|
||||
int lowerOwnerId,
|
||||
int upperOwnerId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, lowerOwnerId],
|
||||
includeLower: includeLower,
|
||||
upper: [checksum, upperOwnerId],
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> remoteIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
@@ -430,29 +667,49 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualToAnyDeviceId(
|
||||
String localId) {
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
value: [null],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId',
|
||||
lower: [null],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualTo(
|
||||
String? localId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'localId',
|
||||
value: [localId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdNotEqualToAnyDeviceId(
|
||||
String localId) {
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdNotEqualTo(
|
||||
String? localId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [],
|
||||
upper: [localId],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [localId],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
@@ -460,13 +717,13 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [localId],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [],
|
||||
upper: [localId],
|
||||
includeUpper: false,
|
||||
@@ -474,151 +731,135 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdDeviceIdEqualTo(
|
||||
String localId, int deviceId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'localId_deviceId',
|
||||
value: [localId, deviceId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
localIdEqualToDeviceIdNotEqualTo(String localId, int deviceId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId],
|
||||
upper: [localId, deviceId],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, deviceId],
|
||||
includeLower: false,
|
||||
upper: [localId],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, deviceId],
|
||||
includeLower: false,
|
||||
upper: [localId],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId],
|
||||
upper: [localId, deviceId],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
localIdEqualToDeviceIdGreaterThan(
|
||||
String localId,
|
||||
int deviceId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, deviceId],
|
||||
includeLower: include,
|
||||
upper: [localId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualToDeviceIdLessThan(
|
||||
String localId,
|
||||
int deviceId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId],
|
||||
upper: [localId, deviceId],
|
||||
includeUpper: include,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualToDeviceIdBetween(
|
||||
String localId,
|
||||
int lowerDeviceId,
|
||||
int upperDeviceId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, lowerDeviceId],
|
||||
includeLower: includeLower,
|
||||
upper: [localId, upperDeviceId],
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdEqualTo(int value) {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdGreaterThan(
|
||||
int value, {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdLessThan(
|
||||
int value, {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumContains(
|
||||
String value,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'checksum',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'checksum',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'checksum',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
@@ -1053,15 +1294,6 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isLocalEqualTo(bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isLocal',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> livePhotoVideoIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
@@ -1210,8 +1442,24 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'localId',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'localId',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdEqualTo(
|
||||
String value, {
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -1224,7 +1472,7 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdGreaterThan(
|
||||
String value, {
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
@@ -1239,7 +1487,7 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdLessThan(
|
||||
String value, {
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
@@ -1254,8 +1502,8 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
@@ -1718,15 +1966,15 @@ extension AssetQueryObject on QueryBuilder<Asset, Asset, QFilterCondition> {}
|
||||
extension AssetQueryLinks on QueryBuilder<Asset, Asset, QFilterCondition> {}
|
||||
|
||||
extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByDeviceId() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByChecksum() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.asc);
|
||||
return query.addSortBy(r'checksum', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByDeviceIdDesc() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByChecksumDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.desc);
|
||||
return query.addSortBy(r'checksum', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1814,18 +2062,6 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsLocal() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsLocalDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByLivePhotoVideoId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
|
||||
@@ -1912,15 +2148,15 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||
}
|
||||
|
||||
extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByDeviceId() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByChecksum() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.asc);
|
||||
return query.addSortBy(r'checksum', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByDeviceIdDesc() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByChecksumDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.desc);
|
||||
return query.addSortBy(r'checksum', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2020,18 +2256,6 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsLocal() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsLocalDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByLivePhotoVideoId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
|
||||
@@ -2118,9 +2342,10 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||
}
|
||||
|
||||
extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByDeviceId() {
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByChecksum(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'deviceId');
|
||||
return query.addDistinctBy(r'checksum', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2167,12 +2392,6 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsLocal() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'isLocal');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByLivePhotoVideoId(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -2227,9 +2446,9 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, int, QQueryOperations> deviceIdProperty() {
|
||||
QueryBuilder<Asset, String, QQueryOperations> checksumProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'deviceId');
|
||||
return query.addPropertyName(r'checksum');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2275,19 +2494,13 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, bool, QQueryOperations> isLocalProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isLocal');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, String?, QQueryOperations> livePhotoVideoIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'livePhotoVideoId');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, String, QQueryOperations> localIdProperty() {
|
||||
QueryBuilder<Asset, String?, QQueryOperations> localIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'localId');
|
||||
});
|
||||
|
||||
8
mobile/lib/shared/models/device_asset.dart
Normal file
8
mobile/lib/shared/models/device_asset.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class DeviceAsset {
|
||||
DeviceAsset({required this.hash});
|
||||
|
||||
@Index(unique: false, type: IndexType.hash)
|
||||
List<byte> hash;
|
||||
}
|
||||
14
mobile/lib/shared/models/ios_device_asset.dart
Normal file
14
mobile/lib/shared/models/ios_device_asset.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'ios_device_asset.g.dart';
|
||||
|
||||
@Collection()
|
||||
class IOSDeviceAsset extends DeviceAsset {
|
||||
IOSDeviceAsset({required this.id, required super.hash});
|
||||
|
||||
@Index(replace: true, unique: true, type: IndexType.hash)
|
||||
String id;
|
||||
Id get isarId => fastHash(id);
|
||||
}
|
||||
780
mobile/lib/shared/models/ios_device_asset.g.dart
Normal file
780
mobile/lib/shared/models/ios_device_asset.g.dart
Normal file
@@ -0,0 +1,780 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'ios_device_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetIOSDeviceAssetCollection on Isar {
|
||||
IsarCollection<IOSDeviceAsset> get iOSDeviceAssets => this.collection();
|
||||
}
|
||||
|
||||
const IOSDeviceAssetSchema = CollectionSchema(
|
||||
name: r'IOSDeviceAsset',
|
||||
id: -1671546753821948030,
|
||||
properties: {
|
||||
r'hash': PropertySchema(
|
||||
id: 0,
|
||||
name: r'hash',
|
||||
type: IsarType.byteList,
|
||||
),
|
||||
r'id': PropertySchema(
|
||||
id: 1,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
)
|
||||
},
|
||||
estimateSize: _iOSDeviceAssetEstimateSize,
|
||||
serialize: _iOSDeviceAssetSerialize,
|
||||
deserialize: _iOSDeviceAssetDeserialize,
|
||||
deserializeProp: _iOSDeviceAssetDeserializeProp,
|
||||
idName: r'isarId',
|
||||
indexes: {
|
||||
r'id': IndexSchema(
|
||||
id: -3268401673993471357,
|
||||
name: r'id',
|
||||
unique: true,
|
||||
replace: true,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'id',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: true,
|
||||
)
|
||||
],
|
||||
),
|
||||
r'hash': IndexSchema(
|
||||
id: -7973251393006690288,
|
||||
name: r'hash',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'hash',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _iOSDeviceAssetGetId,
|
||||
getLinks: _iOSDeviceAssetGetLinks,
|
||||
attach: _iOSDeviceAssetAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _iOSDeviceAssetEstimateSize(
|
||||
IOSDeviceAsset object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.hash.length;
|
||||
bytesCount += 3 + object.id.length * 3;
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _iOSDeviceAssetSerialize(
|
||||
IOSDeviceAsset object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeByteList(offsets[0], object.hash);
|
||||
writer.writeString(offsets[1], object.id);
|
||||
}
|
||||
|
||||
IOSDeviceAsset _iOSDeviceAssetDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = IOSDeviceAsset(
|
||||
hash: reader.readByteList(offsets[0]) ?? [],
|
||||
id: reader.readString(offsets[1]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _iOSDeviceAssetDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
case 1:
|
||||
return (reader.readString(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _iOSDeviceAssetGetId(IOSDeviceAsset object) {
|
||||
return object.isarId;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _iOSDeviceAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, IOSDeviceAsset object) {}
|
||||
|
||||
extension IOSDeviceAssetByIndex on IsarCollection<IOSDeviceAsset> {
|
||||
Future<IOSDeviceAsset?> getById(String id) {
|
||||
return getByIndex(r'id', [id]);
|
||||
}
|
||||
|
||||
IOSDeviceAsset? getByIdSync(String id) {
|
||||
return getByIndexSync(r'id', [id]);
|
||||
}
|
||||
|
||||
Future<bool> deleteById(String id) {
|
||||
return deleteByIndex(r'id', [id]);
|
||||
}
|
||||
|
||||
bool deleteByIdSync(String id) {
|
||||
return deleteByIndexSync(r'id', [id]);
|
||||
}
|
||||
|
||||
Future<List<IOSDeviceAsset?>> getAllById(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return getAllByIndex(r'id', values);
|
||||
}
|
||||
|
||||
List<IOSDeviceAsset?> getAllByIdSync(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return getAllByIndexSync(r'id', values);
|
||||
}
|
||||
|
||||
Future<int> deleteAllById(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return deleteAllByIndex(r'id', values);
|
||||
}
|
||||
|
||||
int deleteAllByIdSync(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return deleteAllByIndexSync(r'id', values);
|
||||
}
|
||||
|
||||
Future<Id> putById(IOSDeviceAsset object) {
|
||||
return putByIndex(r'id', object);
|
||||
}
|
||||
|
||||
Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) {
|
||||
return putByIndexSync(r'id', object, saveLinks: saveLinks);
|
||||
}
|
||||
|
||||
Future<List<Id>> putAllById(List<IOSDeviceAsset> objects) {
|
||||
return putAllByIndex(r'id', objects);
|
||||
}
|
||||
|
||||
List<Id> putAllByIdSync(List<IOSDeviceAsset> objects,
|
||||
{bool saveLinks = true}) {
|
||||
return putAllByIndexSync(r'id', objects, saveLinks: saveLinks);
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhereSort
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhere> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhere> anyIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhere
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhereClause> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdEqualTo(
|
||||
Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: isarId,
|
||||
upper: isarId,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdNotEqualTo(Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdGreaterThan(Id isarId, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdLessThan(Id isarId, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdBetween(
|
||||
Id lowerIsarId,
|
||||
Id upperIsarId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerIsarId,
|
||||
includeLower: includeLower,
|
||||
upper: upperIsarId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idEqualTo(
|
||||
String id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'id',
|
||||
value: [id],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idNotEqualTo(
|
||||
String id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [],
|
||||
upper: [id],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [id],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [id],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [],
|
||||
upper: [id],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> hashEqualTo(
|
||||
List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'hash',
|
||||
value: [hash],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
hashNotEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryFilter
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'hash',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'id',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'id',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdEqualTo(Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'isarId',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryObject
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension IOSDeviceAssetQueryLinks
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension IOSDeviceAssetQuerySortBy
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortBy> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQuerySortThenBy
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortThenBy> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy>
|
||||
thenByIsarIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhereDistinct
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctByHash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctById(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryProperty
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QQueryProperty> {
|
||||
QueryBuilder<IOSDeviceAsset, int, QQueryOperations> isarIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isarId');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, List<int>, QQueryOperations> hashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, String, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,7 @@ import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
/// State does not contain archived assets.
|
||||
/// Use database provider if you want to access the isArchived assets
|
||||
class AssetsState {}
|
||||
|
||||
class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
class AssetNotifier extends StateNotifier<bool> {
|
||||
final AssetService _assetService;
|
||||
final AlbumService _albumService;
|
||||
final UserService _userService;
|
||||
@@ -38,7 +34,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
this._userService,
|
||||
this._syncService,
|
||||
this._db,
|
||||
) : super(AssetsState());
|
||||
) : super(false);
|
||||
|
||||
Future<void> getAllAsset({bool clear = false}) async {
|
||||
if (_getAllAssetInProgress || _deleteInProgress) {
|
||||
@@ -48,14 +44,15 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
_getAllAssetInProgress = true;
|
||||
state = true;
|
||||
if (clear) {
|
||||
await clearAssetsAndAlbums(_db);
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
}
|
||||
await _userService.refreshUsers();
|
||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||
await _userService.refreshUsers();
|
||||
final List<User> partners =
|
||||
await _db.users.filter().isPartnerSharedWithEqualTo(true).findAll();
|
||||
for (User u in partners) {
|
||||
@@ -64,6 +61,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} finally {
|
||||
_getAllAssetInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +77,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
|
||||
Future<void> deleteAssets(Set<Asset> deleteAssets) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
||||
@@ -91,24 +90,14 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
}
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _deleteLocalAssets(Set<Asset> assetsToDelete) async {
|
||||
final int deviceId = Store.get(StoreKey.deviceIdHash);
|
||||
final List<String> local = [];
|
||||
final List<String> local =
|
||||
assetsToDelete.where((a) => a.isLocal).map((a) => a.localId!).toList();
|
||||
// Delete asset from device
|
||||
for (final Asset asset in assetsToDelete) {
|
||||
if (asset.isLocal) {
|
||||
local.add(asset.localId);
|
||||
} else if (asset.deviceId == deviceId) {
|
||||
// Delete asset on device if it is still present
|
||||
var localAsset = await AssetEntity.fromId(asset.localId);
|
||||
if (localAsset != null) {
|
||||
local.add(localAsset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (local.isNotEmpty) {
|
||||
try {
|
||||
return await PhotoManager.editor.deleteWithIds(local);
|
||||
@@ -153,7 +142,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
}
|
||||
}
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
@@ -201,7 +190,7 @@ final remoteAssetsProvider =
|
||||
.remoteIdIsNotNull()
|
||||
.filter()
|
||||
.ownerIdEqualTo(userId)
|
||||
.sortByFileCreatedAt();
|
||||
.sortByFileCreatedAtDesc();
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||
|
||||
@@ -76,7 +76,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
||||
return {
|
||||
"major": int.parse(major),
|
||||
"minor": int.parse(minor),
|
||||
"patch": int.parse(patch),
|
||||
"patch": int.parse(patch.replaceAll("-DEBUG", "")),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,11 @@ class AssetService {
|
||||
try {
|
||||
final etag = hasCache ? _db.eTags.getByIdSync(user.id)?.value : null;
|
||||
final (List<AssetResponseDto>? assets, String? newETag) =
|
||||
await _apiService.assetApi
|
||||
.getAllAssetsWithETag(eTag: etag, userId: user.id);
|
||||
await _apiService.assetApi.getAllAssetsWithETag(
|
||||
eTag: etag,
|
||||
userId: user.id,
|
||||
withoutThumbs: true,
|
||||
);
|
||||
if (assets == null) {
|
||||
return null;
|
||||
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
|
||||
|
||||
175
mobile/lib/shared/services/hash.service.dart
Normal file
175
mobile/lib/shared/services/hash.service.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/shared/models/android_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/ios_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class HashService {
|
||||
HashService(this._db, this._backgroundService);
|
||||
final Isar _db;
|
||||
final BackgroundService _backgroundService;
|
||||
final _log = Logger('HashService');
|
||||
|
||||
/// Returns all assets that were successfully hashed
|
||||
Future<List<Asset>> getHashedAssets(
|
||||
AssetPathEntity album, {
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
Set<String>? excludedAssets,
|
||||
}) async {
|
||||
final entities = await album.getAssetListRange(start: start, end: end);
|
||||
final filtered = excludedAssets == null
|
||||
? entities
|
||||
: entities.where((e) => !excludedAssets.contains(e.id)).toList();
|
||||
return _hashAssets(filtered);
|
||||
}
|
||||
|
||||
/// Converts a list of [AssetEntity]s to [Asset]s including only those
|
||||
/// that were successfully hashed. Hashes are looked up in a DB table
|
||||
/// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
|
||||
/// entries are newly hashed and added to the DB table.
|
||||
Future<List<Asset>> _hashAssets(List<AssetEntity> assetEntities) async {
|
||||
const int batchFileCount = 128;
|
||||
const int batchDataSize = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
final ids = assetEntities
|
||||
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
||||
.toList();
|
||||
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
||||
final List<DeviceAsset> toAdd = [];
|
||||
final List<String> toHash = [];
|
||||
|
||||
int bytes = 0;
|
||||
|
||||
for (int i = 0; i < assetEntities.length; i++) {
|
||||
if (hashes[i] != null) {
|
||||
continue;
|
||||
}
|
||||
final file = await assetEntities[i].originFile;
|
||||
if (file == null) {
|
||||
_log.warning(
|
||||
"Failed to get file for asset ${assetEntities[i].id}, skipping",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
bytes += await file.length();
|
||||
toHash.add(file.path);
|
||||
final deviceAsset = Platform.isAndroid
|
||||
? AndroidDeviceAsset(id: ids[i] as int, hash: const [])
|
||||
: IOSDeviceAsset(id: ids[i] as String, hash: const []);
|
||||
toAdd.add(deviceAsset);
|
||||
hashes[i] = deviceAsset;
|
||||
if (toHash.length == batchFileCount || bytes >= batchDataSize) {
|
||||
await _processBatch(toHash, toAdd);
|
||||
toAdd.clear();
|
||||
toHash.clear();
|
||||
bytes = 0;
|
||||
}
|
||||
}
|
||||
if (toHash.isNotEmpty) {
|
||||
await _processBatch(toHash, toAdd);
|
||||
}
|
||||
return _mapAllHashedAssets(assetEntities, hashes);
|
||||
}
|
||||
|
||||
/// Lookup hashes of assets by their local ID
|
||||
Future<List<DeviceAsset?>> _lookupHashes(List<Object> ids) =>
|
||||
Platform.isAndroid
|
||||
? _db.androidDeviceAssets.getAll(ids.cast())
|
||||
: _db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
|
||||
/// Processes a batch of files and saves any successfully hashed
|
||||
/// values to the DB table.
|
||||
Future<void> _processBatch(
|
||||
final List<String> toHash,
|
||||
final List<DeviceAsset> toAdd,
|
||||
) async {
|
||||
final hashes = await _hashFiles(toHash);
|
||||
bool anyNull = false;
|
||||
for (int j = 0; j < hashes.length; j++) {
|
||||
if (hashes[j]?.length == 20) {
|
||||
toAdd[j].hash = hashes[j]!;
|
||||
} else {
|
||||
_log.warning("Failed to hash file ${toHash[j]}, skipping");
|
||||
anyNull = true;
|
||||
}
|
||||
}
|
||||
final validHashes = anyNull
|
||||
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
|
||||
: toAdd;
|
||||
await _db.writeTxn(
|
||||
() => Platform.isAndroid
|
||||
? _db.androidDeviceAssets.putAll(validHashes.cast())
|
||||
: _db.iOSDeviceAssets.putAll(validHashes.cast()),
|
||||
);
|
||||
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
|
||||
}
|
||||
|
||||
/// Hashes the given files and returns a list of the same length
|
||||
/// files that could not be hashed have a `null` value
|
||||
Future<List<Uint8List?>> _hashFiles(List<String> paths) async {
|
||||
if (Platform.isAndroid) {
|
||||
final List<Uint8List?>? hashes =
|
||||
await _backgroundService.digestFiles(paths);
|
||||
if (hashes == null) {
|
||||
throw Exception("Hashing ${paths.length} files failed");
|
||||
}
|
||||
return hashes;
|
||||
} else if (Platform.isIOS) {
|
||||
final List<Uint8List?> result = List.filled(paths.length, null);
|
||||
for (int i = 0; i < paths.length; i++) {
|
||||
result[i] = await _hashAssetDart(File(paths[i]));
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
throw Exception("_hashFiles implementation missing");
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes a single file using Dart's crypto package
|
||||
Future<Uint8List?> _hashAssetDart(File f) async {
|
||||
late Digest output;
|
||||
final sink = sha1.startChunkedConversion(
|
||||
ChunkedConversionSink<Digest>.withCallback((accumulated) {
|
||||
output = accumulated.first;
|
||||
}),
|
||||
);
|
||||
await for (final chunk in f.openRead()) {
|
||||
sink.add(chunk);
|
||||
}
|
||||
sink.close();
|
||||
return Uint8List.fromList(output.bytes);
|
||||
}
|
||||
|
||||
/// Converts [AssetEntity]s that were successfully hashed to [Asset]s
|
||||
List<Asset> _mapAllHashedAssets(
|
||||
List<AssetEntity> assets,
|
||||
List<DeviceAsset?> hashes,
|
||||
) {
|
||||
final List<Asset> result = [];
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) {
|
||||
result.add(Asset.local(assets[i], hashes[i]!.hash));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
final hashServiceProvider = Provider(
|
||||
(ref) => HashService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
),
|
||||
);
|
||||
@@ -4,10 +4,12 @@ import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
@@ -16,15 +18,17 @@ import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final syncServiceProvider =
|
||||
Provider((ref) => SyncService(ref.watch(dbProvider)));
|
||||
final syncServiceProvider = Provider(
|
||||
(ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)),
|
||||
);
|
||||
|
||||
class SyncService {
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
|
||||
SyncService(this._db);
|
||||
SyncService(this._db, this._hashService);
|
||||
|
||||
// public methods:
|
||||
|
||||
@@ -33,6 +37,7 @@ class SyncService {
|
||||
Future<bool> syncUsersFromServer(List<User> users) async {
|
||||
users.sortBy((u) => u.id);
|
||||
final dbUsers = await _db.users.where().sortById().findAll();
|
||||
assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!");
|
||||
final List<int> toDelete = [];
|
||||
final List<User> toUpsert = [];
|
||||
final changes = diffSortedListsSync(
|
||||
@@ -108,40 +113,16 @@ class SyncService {
|
||||
// private methods:
|
||||
|
||||
/// Syncs a new asset to the db. Returns `true` if successful
|
||||
Future<bool> _syncNewAssetToDb(Asset newAsset) async {
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.where()
|
||||
.localIdDeviceIdEqualTo(newAsset.localId, newAsset.deviceId)
|
||||
.findAll();
|
||||
Asset? match;
|
||||
if (inDb.length == 1) {
|
||||
// exactly one match: trivial case
|
||||
match = inDb.first;
|
||||
} else if (inDb.length > 1) {
|
||||
// TODO instead of this heuristics: match by checksum once available
|
||||
for (Asset a in inDb) {
|
||||
if (a.ownerId == newAsset.ownerId &&
|
||||
a.fileModifiedAt.isAtSameMomentAs(newAsset.fileModifiedAt)) {
|
||||
assert(match == null);
|
||||
match = a;
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
for (Asset a in inDb) {
|
||||
if (a.ownerId == newAsset.ownerId) {
|
||||
assert(match == null);
|
||||
match = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match != null) {
|
||||
Future<bool> _syncNewAssetToDb(Asset a) async {
|
||||
final Asset? inDb =
|
||||
await _db.assets.getByChecksumOwnerId(a.checksum, a.ownerId);
|
||||
if (inDb != null) {
|
||||
// unify local/remote assets by replacing the
|
||||
// local-only asset in the DB with a local&remote asset
|
||||
newAsset = match.updatedCopy(newAsset);
|
||||
a = inDb.updatedCopy(a);
|
||||
}
|
||||
try {
|
||||
await _db.writeTxn(() => newAsset.put(_db));
|
||||
await _db.writeTxn(() => a.put(_db));
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to put new asset into db: $e");
|
||||
return false;
|
||||
@@ -162,11 +143,11 @@ class SyncService {
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(user.isarId)
|
||||
.sortByDeviceId()
|
||||
.thenByLocalId()
|
||||
.thenByFileModifiedAt()
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
remote.sort(Asset.compareByOwnerDeviceLocalIdModified);
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
|
||||
remote.sort(Asset.compareByChecksum);
|
||||
final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true);
|
||||
if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) {
|
||||
return false;
|
||||
@@ -199,6 +180,7 @@ class SyncService {
|
||||
query = baseQuery.owner((q) => q.isarIdEqualTo(me.isarId));
|
||||
}
|
||||
final List<Album> dbAlbums = await query.sortByRemoteId().findAll();
|
||||
assert(dbAlbums.isSortedBy((e) => e.remoteId!), "dbAlbums not sorted!");
|
||||
|
||||
final List<Asset> toDelete = [];
|
||||
final List<Asset> existing = [];
|
||||
@@ -245,16 +227,16 @@ class SyncService {
|
||||
if (dto.assetCount != dto.assets.length) {
|
||||
return false;
|
||||
}
|
||||
final assetsInDb = await album.assets
|
||||
.filter()
|
||||
.sortByOwnerId()
|
||||
.thenByDeviceId()
|
||||
.thenByLocalId()
|
||||
.thenByFileModifiedAt()
|
||||
.findAll();
|
||||
final assetsInDb =
|
||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.getAssets();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerDeviceLocalIdModified);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb);
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||
assetsOnRemote,
|
||||
assetsInDb,
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
);
|
||||
|
||||
// update shared users
|
||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||
@@ -297,6 +279,7 @@ class SyncService {
|
||||
await album.assets.update(link: assetsToLink, unlink: toUnlink.cast());
|
||||
await _db.albums.put(album);
|
||||
});
|
||||
_log.info("Synced changes of remote album ${album.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote album to database $e");
|
||||
}
|
||||
@@ -382,10 +365,11 @@ class SyncService {
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||
final List<Album> inDb =
|
||||
final inDb =
|
||||
await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll();
|
||||
final List<Asset> deleteCandidates = [];
|
||||
final List<Asset> existing = [];
|
||||
assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!");
|
||||
final bool anyChanges = await diffSortedLists(
|
||||
onDevice,
|
||||
inDb,
|
||||
@@ -447,14 +431,15 @@ class SyncService {
|
||||
final inDb = await album.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.deviceIdEqualTo(Store.get(StoreKey.deviceIdHash))
|
||||
.sortByLocalId()
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice = await ape.assetCountAsync;
|
||||
final List<Asset> onDevice =
|
||||
await ape.getAssets(excludedAssets: excludedAssets);
|
||||
onDevice.sort(Asset.compareByLocalId);
|
||||
final (toAdd, toUpdate, toDelete) =
|
||||
_diffAssets(onDevice, inDb, compare: Asset.compareByLocalId);
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(onDevice);
|
||||
// _removeDuplicates sorts `onDevice` by checksum
|
||||
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
||||
if (toAdd.isEmpty &&
|
||||
toUpdate.isEmpty &&
|
||||
toDelete.isEmpty &&
|
||||
@@ -491,6 +476,9 @@ class SyncService {
|
||||
await _db.albums.put(album);
|
||||
album.thumbnail.value ??= await album.assets.filter().findFirst();
|
||||
await album.thumbnail.save();
|
||||
await _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, value: assetCountOnDevice.toString()),
|
||||
);
|
||||
});
|
||||
_log.info("Synced changes of local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
@@ -503,8 +491,13 @@ class SyncService {
|
||||
/// fast path for common case: only new assets were added to device album
|
||||
/// returns `true` if successfull, else `false`
|
||||
Future<bool> _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async {
|
||||
if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
|
||||
return false;
|
||||
}
|
||||
final int totalOnDevice = await ape.assetCountAsync;
|
||||
final AssetPathEntity? modified = totalOnDevice > album.assetCount
|
||||
final int lastKnownTotal =
|
||||
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.value?.toInt() ?? 0;
|
||||
final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
|
||||
? await ape.fetchPathProperties(
|
||||
filterOptionGroup: FilterOptionGroup(
|
||||
updateTimeCond: DateTimeCond(
|
||||
@@ -517,17 +510,22 @@ class SyncService {
|
||||
if (modified == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Asset> newAssets = await modified.getAssets();
|
||||
if (totalOnDevice != album.assets.length + newAssets.length) {
|
||||
final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
|
||||
|
||||
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
||||
return false;
|
||||
}
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
_removeDuplicates(newAssets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await album.assets.update(link: existingInDb + updated);
|
||||
await _db.albums.put(album);
|
||||
await _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, value: totalOnDevice.toString()),
|
||||
);
|
||||
});
|
||||
_log.info("Fast synced local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
@@ -547,7 +545,9 @@ class SyncService {
|
||||
]) async {
|
||||
_log.info("Syncing a new local album to DB: ${ape.name}");
|
||||
final Album a = Album.local(ape);
|
||||
final assets = await ape.getAssets(excludedAssets: excludedAssets);
|
||||
final assets =
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(assets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||
_log.info(
|
||||
"${existingInDb.length} assets already existed in DB, to upsert ${updated.length}",
|
||||
@@ -570,44 +570,29 @@ class SyncService {
|
||||
Future<(List<Asset> existing, List<Asset> updated)> _linkWithExistingFromDb(
|
||||
List<Asset> assets,
|
||||
) async {
|
||||
if (assets.isEmpty) {
|
||||
return ([].cast<Asset>(), [].cast<Asset>());
|
||||
}
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.where()
|
||||
.anyOf(
|
||||
assets,
|
||||
(q, Asset e) => q.localIdDeviceIdEqualTo(e.localId, e.deviceId),
|
||||
)
|
||||
.sortByOwnerId()
|
||||
.thenByDeviceId()
|
||||
.thenByLocalId()
|
||||
.thenByFileModifiedAt()
|
||||
.findAll();
|
||||
assets.sort(Asset.compareByOwnerDeviceLocalIdModified);
|
||||
final List<Asset> existing = [], toUpsert = [];
|
||||
diffSortedListsSync(
|
||||
inDb,
|
||||
assets,
|
||||
// do not compare by modified date because for some assets dates differ on
|
||||
// client and server, thus never reaching "both" case below
|
||||
compare: Asset.compareByOwnerDeviceLocalId,
|
||||
both: (Asset a, Asset b) {
|
||||
if (a.canUpdate(b)) {
|
||||
toUpsert.add(a.updatedCopy(b));
|
||||
return true;
|
||||
} else {
|
||||
existing.add(a);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onlyFirst: (Asset a) => _log.finer(
|
||||
"_linkWithExistingFromDb encountered asset only in DB: $a",
|
||||
null,
|
||||
StackTrace.current,
|
||||
),
|
||||
onlySecond: (Asset b) => toUpsert.add(b),
|
||||
if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>());
|
||||
|
||||
final List<Asset?> inDb = await _db.assets.getAllByChecksumOwnerId(
|
||||
assets.map((a) => a.checksum).toList(growable: false),
|
||||
assets.map((a) => a.ownerId).toInt64List(),
|
||||
);
|
||||
assert(inDb.length == assets.length);
|
||||
final List<Asset> existing = [], toUpsert = [];
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
toUpsert.add(assets[i]);
|
||||
continue;
|
||||
}
|
||||
if (b.canUpdate(assets[i])) {
|
||||
final updated = b.updatedCopy(assets[i]);
|
||||
assert(updated.id != Isar.autoIncrement);
|
||||
toUpsert.add(updated);
|
||||
} else {
|
||||
existing.add(b);
|
||||
}
|
||||
}
|
||||
assert(existing.length + toUpsert.length == assets.length);
|
||||
return (existing, toUpsert);
|
||||
}
|
||||
|
||||
@@ -627,11 +612,63 @@ class SyncService {
|
||||
});
|
||||
_log.info("Upserted ${assets.length} assets into the DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.warning(
|
||||
_log.severe(
|
||||
"Failed to upsert ${assets.length} assets into the DB: ${e.toString()}",
|
||||
);
|
||||
// give details on the errors
|
||||
assets.sort(Asset.compareByOwnerChecksum);
|
||||
final inDb = await _db.assets.getAllByChecksumOwnerId(
|
||||
assets.map((e) => e.checksum).toList(growable: false),
|
||||
assets.map((e) => e.ownerId).toInt64List(),
|
||||
);
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
final Asset a = assets[i];
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
if (a.id != Isar.autoIncrement) {
|
||||
_log.warning(
|
||||
"Trying to update an asset that does not exist in DB:\n$a",
|
||||
);
|
||||
}
|
||||
} else if (a.id != b.id) {
|
||||
_log.warning(
|
||||
"Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a",
|
||||
);
|
||||
}
|
||||
}
|
||||
for (int i = 1; i < assets.length; i++) {
|
||||
if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) {
|
||||
_log.warning(
|
||||
"Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Asset> _removeDuplicates(List<Asset> assets) {
|
||||
final int before = assets.length;
|
||||
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
||||
assets.uniqueConsecutive(
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
onDuplicate: (a, b) =>
|
||||
_log.info("Ignoring duplicate assets on device:\n$a\n$b"),
|
||||
);
|
||||
final int duplicates = before - assets.length;
|
||||
if (duplicates > 0) {
|
||||
_log.warning("Ignored $duplicates duplicate assets on device");
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
|
||||
return a.name != b.name ||
|
||||
a.lastModified == null ||
|
||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
|
||||
await a.assetCountAsync !=
|
||||
(await _db.eTags.getById(a.eTagKeyAssetCount))?.value?.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a triple(toAdd, toUpdate, toRemove)
|
||||
@@ -639,7 +676,7 @@ class SyncService {
|
||||
List<Asset> assets,
|
||||
List<Asset> inDb, {
|
||||
bool? remote,
|
||||
int Function(Asset, Asset) compare = Asset.compareByOwnerDeviceLocalId,
|
||||
int Function(Asset, Asset) compare = Asset.compareByChecksum,
|
||||
}) {
|
||||
final List<Asset> toAdd = [];
|
||||
final List<Asset> toUpdate = [];
|
||||
@@ -663,7 +700,7 @@ class SyncService {
|
||||
}
|
||||
} else if (remote == false && a.isRemote) {
|
||||
if (a.isLocal) {
|
||||
a.isLocal = false;
|
||||
a.localId = null;
|
||||
toUpdate.add(a);
|
||||
}
|
||||
} else {
|
||||
@@ -685,9 +722,9 @@ class SyncService {
|
||||
return const ([], []);
|
||||
}
|
||||
deleteCandidates.sort(Asset.compareById);
|
||||
deleteCandidates.uniqueConsecutive((a) => a.id);
|
||||
deleteCandidates.uniqueConsecutive(compare: Asset.compareById);
|
||||
existing.sort(Asset.compareById);
|
||||
existing.uniqueConsecutive((a) => a.id);
|
||||
existing.uniqueConsecutive(compare: Asset.compareById);
|
||||
final (tooAdd, toUpdate, toRemove) = _diffAssets(
|
||||
existing,
|
||||
deleteCandidates,
|
||||
@@ -698,14 +735,6 @@ class SyncService {
|
||||
return (toRemove.map((e) => e.id).toList(), toUpdate);
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
|
||||
return a.name != b.name ||
|
||||
a.lastModified == null ||
|
||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
|
||||
await a.assetCountAsync != b.assetCount;
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
||||
return dto.assetCount != a.assetCount ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
@@ -110,8 +111,12 @@ class ImmichImage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
debugPrint("Error getting thumbnail $url = $error");
|
||||
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
||||
if (error is HttpExceptionWithStatus &&
|
||||
error.statusCode >= 400 &&
|
||||
error.statusCode < 500) {
|
||||
debugPrint("Evicting thumbnail '$url' from cache: $error");
|
||||
CachedNetworkImage.evictFromCache(url);
|
||||
}
|
||||
return Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: Theme.of(context).primaryColor,
|
||||
|
||||
@@ -6,12 +6,39 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
|
||||
class TabControllerPage extends ConsumerWidget {
|
||||
class TabControllerPage extends HookConsumerWidget {
|
||||
const TabControllerPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final refreshing = ref.watch(assetProvider);
|
||||
|
||||
Widget buildIcon(Widget icon) {
|
||||
if (!refreshing) return icon;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -14,
|
||||
child: SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
navigationRail(TabsRouter tabsRouter) {
|
||||
return NavigationRail(
|
||||
labelType: NavigationRailLabelType.all,
|
||||
@@ -83,9 +110,12 @@ class TabControllerPage extends ConsumerWidget {
|
||||
icon: const Icon(
|
||||
Icons.photo_library_outlined,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.photo_library,
|
||||
color: Theme.of(context).primaryColor,
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
size: 24,
|
||||
Icons.photo_library,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
@@ -113,9 +143,11 @@ class TabControllerPage extends ConsumerWidget {
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
extension DurationExtension on String {
|
||||
@@ -22,15 +24,20 @@ extension DurationExtension on String {
|
||||
}
|
||||
|
||||
extension ListExtension<E> on List<E> {
|
||||
List<E> uniqueConsecutive<T>([T Function(E element)? key]) {
|
||||
key ??= (E e) => e as T;
|
||||
List<E> uniqueConsecutive({
|
||||
int Function(E a, E b)? compare,
|
||||
void Function(E a, E b)? onDuplicate,
|
||||
}) {
|
||||
compare ??= (E a, E b) => a == b ? 0 : 1;
|
||||
int i = 1, j = 1;
|
||||
for (; i < length; i++) {
|
||||
if (key(this[i]) != key(this[i - 1])) {
|
||||
if (compare(this[i - 1], this[i]) != 0) {
|
||||
if (i != j) {
|
||||
this[j] = this[i];
|
||||
}
|
||||
j++;
|
||||
} else if (onDuplicate != null) {
|
||||
onDuplicate(this[i - 1], this[i]);
|
||||
}
|
||||
}
|
||||
length = length == 0 ? 0 : j;
|
||||
@@ -45,3 +52,11 @@ extension ListExtension<E> on List<E> {
|
||||
return ListSlice<E>(this, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
extension IntListExtension on Iterable<int> {
|
||||
Int64List toInt64List() {
|
||||
final list = Int64List(length);
|
||||
list.setAll(0, this);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,15 @@ class FileHelper {
|
||||
case 'webm':
|
||||
return {"type": "video", "subType": "webm"};
|
||||
|
||||
case 'insp':
|
||||
return {"type": "image", "subType": "jpeg"};
|
||||
|
||||
case 'insv':
|
||||
return {"type": "video", "subType": "mp4"};
|
||||
|
||||
case 'arw':
|
||||
return {"type": "image", "subType": "x-sony-arw"};
|
||||
|
||||
default:
|
||||
return {"type": "unsupport", "subType": "unsupport"};
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||
final int version = Store.get(StoreKey.version, 1);
|
||||
switch (version) {
|
||||
case 1:
|
||||
await _migrateV1ToV2(db);
|
||||
await _migrateTo(db, 2);
|
||||
case 2:
|
||||
await _migrateTo(db, 3);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _migrateV1ToV2(Isar db) async {
|
||||
Future<void> _migrateTo(Isar db, int version) async {
|
||||
await clearAssetsAndAlbums(db);
|
||||
await Store.put(StoreKey.version, 2);
|
||||
await Store.put(StoreKey.version, version);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,16 @@ extension WithETag on AssetApi {
|
||||
Future<(List<AssetResponseDto>? assets, String? eTag)> getAllAssetsWithETag({
|
||||
String? eTag,
|
||||
String? userId,
|
||||
bool? isFavorite,
|
||||
bool? isArchived,
|
||||
bool? withoutThumbs,
|
||||
}) async {
|
||||
final response = await getAllAssetsWithHttpInfo(
|
||||
ifNoneMatch: eTag,
|
||||
userId: userId,
|
||||
isFavorite: isFavorite,
|
||||
isArchived: isArchived,
|
||||
withoutThumbs: withoutThumbs,
|
||||
);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
|
||||
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@@ -63,6 +63,7 @@ doc/LoginCredentialDto.md
|
||||
doc/LoginResponseDto.md
|
||||
doc/LogoutResponseDto.md
|
||||
doc/MapMarkerResponseDto.md
|
||||
doc/MemoryLaneResponseDto.md
|
||||
doc/OAuthApi.md
|
||||
doc/OAuthCallbackDto.md
|
||||
doc/OAuthConfigDto.md
|
||||
@@ -194,6 +195,7 @@ lib/model/login_credential_dto.dart
|
||||
lib/model/login_response_dto.dart
|
||||
lib/model/logout_response_dto.dart
|
||||
lib/model/map_marker_response_dto.dart
|
||||
lib/model/memory_lane_response_dto.dart
|
||||
lib/model/o_auth_callback_dto.dart
|
||||
lib/model/o_auth_config_dto.dart
|
||||
lib/model/o_auth_config_response_dto.dart
|
||||
@@ -298,6 +300,7 @@ test/login_credential_dto_test.dart
|
||||
test/login_response_dto_test.dart
|
||||
test/logout_response_dto_test.dart
|
||||
test/map_marker_response_dto_test.dart
|
||||
test/memory_lane_response_dto_test.dart
|
||||
test/o_auth_api_test.dart
|
||||
test/o_auth_callback_dto_test.dart
|
||||
test/o_auth_config_dto_test.dart
|
||||
|
||||
8
mobile/openapi/README.md
generated
8
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.60.0
|
||||
- API version: 1.62.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -83,7 +83,7 @@ Class | Method | HTTP request | Description
|
||||
*AlbumApi* | [**createAlbumSharedLink**](doc//AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
|
||||
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{id} |
|
||||
*AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{id}/download |
|
||||
*AlbumApi* | [**getAlbumCountByUserId**](doc//AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
|
||||
*AlbumApi* | [**getAlbumCount**](doc//AlbumApi.md#getalbumcount) | **GET** /album/count |
|
||||
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{id} |
|
||||
*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album |
|
||||
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
|
||||
@@ -109,6 +109,7 @@ Class | Method | HTTP request | Description
|
||||
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
||||
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
||||
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||
*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
||||
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
||||
@@ -124,7 +125,7 @@ Class | Method | HTTP request | Description
|
||||
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
|
||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
|
||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
|
||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||
*OAuthApi* | [**callback**](doc//OAuthApi.md#callback) | **POST** /oauth/callback |
|
||||
*OAuthApi* | [**generateConfig**](doc//OAuthApi.md#generateconfig) | **POST** /oauth/config |
|
||||
*OAuthApi* | [**link**](doc//OAuthApi.md#link) | **POST** /oauth/link |
|
||||
@@ -231,6 +232,7 @@ Class | Method | HTTP request | Description
|
||||
- [LoginResponseDto](doc//LoginResponseDto.md)
|
||||
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
||||
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
||||
- [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md)
|
||||
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
|
||||
|
||||
10
mobile/openapi/doc/AlbumApi.md
generated
10
mobile/openapi/doc/AlbumApi.md
generated
@@ -15,7 +15,7 @@ Method | HTTP request | Description
|
||||
[**createAlbumSharedLink**](AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
|
||||
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{id} |
|
||||
[**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{id}/download |
|
||||
[**getAlbumCountByUserId**](AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
|
||||
[**getAlbumCount**](AlbumApi.md#getalbumcount) | **GET** /album/count |
|
||||
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{id} |
|
||||
[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album |
|
||||
[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
|
||||
@@ -364,8 +364,8 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getAlbumCountByUserId**
|
||||
> AlbumCountResponseDto getAlbumCountByUserId()
|
||||
# **getAlbumCount**
|
||||
> AlbumCountResponseDto getAlbumCount()
|
||||
|
||||
|
||||
|
||||
@@ -390,10 +390,10 @@ import 'package:openapi/api.dart';
|
||||
final api_instance = AlbumApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getAlbumCountByUserId();
|
||||
final result = api_instance.getAlbumCount();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AlbumApi->getAlbumCountByUserId: $e\n');
|
||||
print('Exception when calling AlbumApi->getAlbumCount: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
2
mobile/openapi/doc/AlbumCountResponseDto.md
generated
2
mobile/openapi/doc/AlbumCountResponseDto.md
generated
@@ -10,7 +10,7 @@ Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**owned** | **int** | |
|
||||
**shared** | **int** | |
|
||||
**sharing** | **int** | |
|
||||
**notShared** | **int** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
56
mobile/openapi/doc/AssetApi.md
generated
56
mobile/openapi/doc/AssetApi.md
generated
@@ -29,6 +29,7 @@ Method | HTTP request | Description
|
||||
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
||||
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
||||
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||
[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
||||
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
||||
@@ -1161,6 +1162,61 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getMemoryLane**
|
||||
> List<MemoryLaneResponseDto> getMemoryLane(timestamp)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AssetApi();
|
||||
final timestamp = 2013-10-20T19:20:30+01:00; // DateTime | Get pictures for +24 hours from this time going back x years
|
||||
|
||||
try {
|
||||
final result = api_instance.getMemoryLane(timestamp);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->getMemoryLane: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**timestamp** | **DateTime**| Get pictures for +24 hours from this time going back x years |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<MemoryLaneResponseDto>**](MemoryLaneResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getUserAssetsByDeviceId**
|
||||
> List<String> getUserAssetsByDeviceId(deviceId)
|
||||
|
||||
|
||||
1
mobile/openapi/doc/AssetResponseDto.md
generated
1
mobile/openapi/doc/AssetResponseDto.md
generated
@@ -16,6 +16,7 @@ Name | Type | Description | Notes
|
||||
**originalPath** | **String** | |
|
||||
**originalFileName** | **String** | |
|
||||
**resized** | **bool** | |
|
||||
**thumbhash** | **String** | base64 encoded thumbhash |
|
||||
**fileCreatedAt** | [**DateTime**](DateTime.md) | |
|
||||
**fileModifiedAt** | [**DateTime**](DateTime.md) | |
|
||||
**updatedAt** | [**DateTime**](DateTime.md) | |
|
||||
|
||||
10
mobile/openapi/doc/JobApi.md
generated
10
mobile/openapi/doc/JobApi.md
generated
@@ -10,7 +10,7 @@ All URIs are relative to */api*
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**getAllJobsStatus**](JobApi.md#getalljobsstatus) | **GET** /jobs |
|
||||
[**sendJobCommand**](JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
|
||||
[**sendJobCommand**](JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||
|
||||
|
||||
# **getAllJobsStatus**
|
||||
@@ -65,7 +65,7 @@ This endpoint does not need any parameter.
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **sendJobCommand**
|
||||
> JobStatusDto sendJobCommand(jobId, jobCommandDto)
|
||||
> JobStatusDto sendJobCommand(id, jobCommandDto)
|
||||
|
||||
|
||||
|
||||
@@ -88,11 +88,11 @@ import 'package:openapi/api.dart';
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = JobApi();
|
||||
final jobId = ; // JobName |
|
||||
final id = ; // JobName |
|
||||
final jobCommandDto = JobCommandDto(); // JobCommandDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.sendJobCommand(jobId, jobCommandDto);
|
||||
final result = api_instance.sendJobCommand(id, jobCommandDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling JobApi->sendJobCommand: $e\n');
|
||||
@@ -103,7 +103,7 @@ try {
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**jobId** | [**JobName**](.md)| |
|
||||
**id** | [**JobName**](.md)| |
|
||||
**jobCommandDto** | [**JobCommandDto**](JobCommandDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
16
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
Normal file
16
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
# openapi.model.MemoryLaneResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**title** | **String** | |
|
||||
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -98,6 +98,7 @@ part 'model/login_credential_dto.dart';
|
||||
part 'model/login_response_dto.dart';
|
||||
part 'model/logout_response_dto.dart';
|
||||
part 'model/map_marker_response_dto.dart';
|
||||
part 'model/memory_lane_response_dto.dart';
|
||||
part 'model/o_auth_callback_dto.dart';
|
||||
part 'model/o_auth_config_dto.dart';
|
||||
part 'model/o_auth_config_response_dto.dart';
|
||||
|
||||
10
mobile/openapi/lib/api/album_api.dart
generated
10
mobile/openapi/lib/api/album_api.dart
generated
@@ -332,10 +332,10 @@ class AlbumApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /album/count-by-user-id' operation and returns the [Response].
|
||||
Future<Response> getAlbumCountByUserIdWithHttpInfo() async {
|
||||
/// Performs an HTTP 'GET /album/count' operation and returns the [Response].
|
||||
Future<Response> getAlbumCountWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/album/count-by-user-id';
|
||||
final path = r'/album/count';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
@@ -358,8 +358,8 @@ class AlbumApi {
|
||||
);
|
||||
}
|
||||
|
||||
Future<AlbumCountResponseDto?> getAlbumCountByUserId() async {
|
||||
final response = await getAlbumCountByUserIdWithHttpInfo();
|
||||
Future<AlbumCountResponseDto?> getAlbumCount() async {
|
||||
final response = await getAlbumCountWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
54
mobile/openapi/lib/api/asset_api.dart
generated
54
mobile/openapi/lib/api/asset_api.dart
generated
@@ -1115,6 +1115,60 @@ class AssetApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /asset/memory-lane' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] timestamp (required):
|
||||
/// Get pictures for +24 hours from this time going back x years
|
||||
Future<Response> getMemoryLaneWithHttpInfo(DateTime timestamp,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/memory-lane';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'timestamp', timestamp));
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] timestamp (required):
|
||||
/// Get pictures for +24 hours from this time going back x years
|
||||
Future<List<MemoryLaneResponseDto>?> getMemoryLane(DateTime timestamp,) async {
|
||||
final response = await getMemoryLaneWithHttpInfo(timestamp,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<MemoryLaneResponseDto>') as List)
|
||||
.cast<MemoryLaneResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get all asset of a device that are in the database, ID only.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
|
||||
16
mobile/openapi/lib/api/job_api.dart
generated
16
mobile/openapi/lib/api/job_api.dart
generated
@@ -57,16 +57,16 @@ class JobApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /jobs/{jobId}' operation and returns the [Response].
|
||||
/// Performs an HTTP 'PUT /jobs/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [JobName] jobId (required):
|
||||
/// * [JobName] id (required):
|
||||
///
|
||||
/// * [JobCommandDto] jobCommandDto (required):
|
||||
Future<Response> sendJobCommandWithHttpInfo(JobName jobId, JobCommandDto jobCommandDto,) async {
|
||||
Future<Response> sendJobCommandWithHttpInfo(JobName id, JobCommandDto jobCommandDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/jobs/{jobId}'
|
||||
.replaceAll('{jobId}', jobId.toString());
|
||||
final path = r'/jobs/{id}'
|
||||
.replaceAll('{id}', id.toString());
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = jobCommandDto;
|
||||
@@ -91,11 +91,11 @@ class JobApi {
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [JobName] jobId (required):
|
||||
/// * [JobName] id (required):
|
||||
///
|
||||
/// * [JobCommandDto] jobCommandDto (required):
|
||||
Future<JobStatusDto?> sendJobCommand(JobName jobId, JobCommandDto jobCommandDto,) async {
|
||||
final response = await sendJobCommandWithHttpInfo(jobId, jobCommandDto,);
|
||||
Future<JobStatusDto?> sendJobCommand(JobName id, JobCommandDto jobCommandDto,) async {
|
||||
final response = await sendJobCommandWithHttpInfo(id, jobCommandDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -291,6 +291,8 @@ class ApiClient {
|
||||
return LogoutResponseDto.fromJson(value);
|
||||
case 'MapMarkerResponseDto':
|
||||
return MapMarkerResponseDto.fromJson(value);
|
||||
case 'MemoryLaneResponseDto':
|
||||
return MemoryLaneResponseDto.fromJson(value);
|
||||
case 'OAuthCallbackDto':
|
||||
return OAuthCallbackDto.fromJson(value);
|
||||
case 'OAuthConfigDto':
|
||||
|
||||
@@ -15,36 +15,36 @@ class AlbumCountResponseDto {
|
||||
AlbumCountResponseDto({
|
||||
required this.owned,
|
||||
required this.shared,
|
||||
required this.sharing,
|
||||
required this.notShared,
|
||||
});
|
||||
|
||||
int owned;
|
||||
|
||||
int shared;
|
||||
|
||||
int sharing;
|
||||
int notShared;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AlbumCountResponseDto &&
|
||||
other.owned == owned &&
|
||||
other.shared == shared &&
|
||||
other.sharing == sharing;
|
||||
other.notShared == notShared;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(owned.hashCode) +
|
||||
(shared.hashCode) +
|
||||
(sharing.hashCode);
|
||||
(notShared.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AlbumCountResponseDto[owned=$owned, shared=$shared, sharing=$sharing]';
|
||||
String toString() => 'AlbumCountResponseDto[owned=$owned, shared=$shared, notShared=$notShared]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'owned'] = this.owned;
|
||||
json[r'shared'] = this.shared;
|
||||
json[r'sharing'] = this.sharing;
|
||||
json[r'notShared'] = this.notShared;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class AlbumCountResponseDto {
|
||||
return AlbumCountResponseDto(
|
||||
owned: mapValueOfType<int>(json, r'owned')!,
|
||||
shared: mapValueOfType<int>(json, r'shared')!,
|
||||
sharing: mapValueOfType<int>(json, r'sharing')!,
|
||||
notShared: mapValueOfType<int>(json, r'notShared')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -119,7 +119,7 @@ class AlbumCountResponseDto {
|
||||
static const requiredKeys = <String>{
|
||||
'owned',
|
||||
'shared',
|
||||
'sharing',
|
||||
'notShared',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
15
mobile/openapi/lib/model/asset_response_dto.dart
generated
15
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -21,6 +21,7 @@ class AssetResponseDto {
|
||||
required this.originalPath,
|
||||
required this.originalFileName,
|
||||
required this.resized,
|
||||
required this.thumbhash,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
required this.updatedAt,
|
||||
@@ -52,6 +53,9 @@ class AssetResponseDto {
|
||||
|
||||
bool resized;
|
||||
|
||||
/// base64 encoded thumbhash
|
||||
String? thumbhash;
|
||||
|
||||
DateTime fileCreatedAt;
|
||||
|
||||
DateTime fileModifiedAt;
|
||||
@@ -101,6 +105,7 @@ class AssetResponseDto {
|
||||
other.originalPath == originalPath &&
|
||||
other.originalFileName == originalFileName &&
|
||||
other.resized == resized &&
|
||||
other.thumbhash == thumbhash &&
|
||||
other.fileCreatedAt == fileCreatedAt &&
|
||||
other.fileModifiedAt == fileModifiedAt &&
|
||||
other.updatedAt == updatedAt &&
|
||||
@@ -126,6 +131,7 @@ class AssetResponseDto {
|
||||
(originalPath.hashCode) +
|
||||
(originalFileName.hashCode) +
|
||||
(resized.hashCode) +
|
||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||
(fileCreatedAt.hashCode) +
|
||||
(fileModifiedAt.hashCode) +
|
||||
(updatedAt.hashCode) +
|
||||
@@ -141,7 +147,7 @@ class AssetResponseDto {
|
||||
(checksum.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resized=$resized, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, mimeType=$mimeType, duration=$duration, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people, checksum=$checksum]';
|
||||
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resized=$resized, thumbhash=$thumbhash, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, mimeType=$mimeType, duration=$duration, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people, checksum=$checksum]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -153,6 +159,11 @@ class AssetResponseDto {
|
||||
json[r'originalPath'] = this.originalPath;
|
||||
json[r'originalFileName'] = this.originalFileName;
|
||||
json[r'resized'] = this.resized;
|
||||
if (this.thumbhash != null) {
|
||||
json[r'thumbhash'] = this.thumbhash;
|
||||
} else {
|
||||
// json[r'thumbhash'] = null;
|
||||
}
|
||||
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
|
||||
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
@@ -212,6 +223,7 @@ class AssetResponseDto {
|
||||
originalPath: mapValueOfType<String>(json, r'originalPath')!,
|
||||
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
||||
resized: mapValueOfType<bool>(json, r'resized')!,
|
||||
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!,
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', '')!,
|
||||
@@ -280,6 +292,7 @@ class AssetResponseDto {
|
||||
'originalPath',
|
||||
'originalFileName',
|
||||
'resized',
|
||||
'thumbhash',
|
||||
'fileCreatedAt',
|
||||
'fileModifiedAt',
|
||||
'updatedAt',
|
||||
|
||||
117
mobile/openapi/lib/model/memory_lane_response_dto.dart
generated
Normal file
117
mobile/openapi/lib/model/memory_lane_response_dto.dart
generated
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MemoryLaneResponseDto {
|
||||
/// Returns a new [MemoryLaneResponseDto] instance.
|
||||
MemoryLaneResponseDto({
|
||||
required this.title,
|
||||
this.assets = const [],
|
||||
});
|
||||
|
||||
String title;
|
||||
|
||||
List<AssetResponseDto> assets;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MemoryLaneResponseDto &&
|
||||
other.title == title &&
|
||||
other.assets == assets;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(title.hashCode) +
|
||||
(assets.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MemoryLaneResponseDto[title=$title, assets=$assets]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'title'] = this.title;
|
||||
json[r'assets'] = this.assets;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MemoryLaneResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MemoryLaneResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
// Ensure that the map contains the required keys.
|
||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||
// Note 2: this code is stripped in release mode!
|
||||
assert(() {
|
||||
requiredKeys.forEach((key) {
|
||||
assert(json.containsKey(key), 'Required key "MemoryLaneResponseDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "MemoryLaneResponseDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return MemoryLaneResponseDto(
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
assets: AssetResponseDto.listFromJson(json[r'assets']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MemoryLaneResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MemoryLaneResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MemoryLaneResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MemoryLaneResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MemoryLaneResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MemoryLaneResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MemoryLaneResponseDto-objects as value to a dart map
|
||||
static Map<String, List<MemoryLaneResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MemoryLaneResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MemoryLaneResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'title',
|
||||
'assets',
|
||||
};
|
||||
}
|
||||
|
||||
4
mobile/openapi/test/album_api_test.dart
generated
4
mobile/openapi/test/album_api_test.dart
generated
@@ -47,8 +47,8 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<AlbumCountResponseDto> getAlbumCountByUserId() async
|
||||
test('test getAlbumCountByUserId', () async {
|
||||
//Future<AlbumCountResponseDto> getAlbumCount() async
|
||||
test('test getAlbumCount', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int sharing
|
||||
test('to test the property `sharing`', () async {
|
||||
// int notShared
|
||||
test('to test the property `notShared`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
5
mobile/openapi/test/asset_api_test.dart
generated
5
mobile/openapi/test/asset_api_test.dart
generated
@@ -129,6 +129,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<MemoryLaneResponseDto>> getMemoryLane(DateTime timestamp) async
|
||||
test('test getMemoryLane', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// Get all asset of a device that are in the database, ID only.
|
||||
//
|
||||
//Future<List<String>> getUserAssetsByDeviceId(String deviceId) async
|
||||
|
||||
6
mobile/openapi/test/asset_response_dto_test.dart
generated
6
mobile/openapi/test/asset_response_dto_test.dart
generated
@@ -56,6 +56,12 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// base64 encoded thumbhash
|
||||
// String thumbhash
|
||||
test('to test the property `thumbhash`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// DateTime fileCreatedAt
|
||||
test('to test the property `fileCreatedAt`', () async {
|
||||
// TODO
|
||||
|
||||
2
mobile/openapi/test/job_api_test.dart
generated
2
mobile/openapi/test/job_api_test.dart
generated
@@ -22,7 +22,7 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<JobStatusDto> sendJobCommand(JobName jobId, JobCommandDto jobCommandDto) async
|
||||
//Future<JobStatusDto> sendJobCommand(JobName id, JobCommandDto jobCommandDto) async
|
||||
test('test sendJobCommand', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
32
mobile/openapi/test/memory_lane_response_dto_test.dart
generated
Normal file
32
mobile/openapi/test/memory_lane_response_dto_test.dart
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for MemoryLaneResponseDto
|
||||
void main() {
|
||||
// final instance = MemoryLaneResponseDto();
|
||||
|
||||
group('test MemoryLaneResponseDto', () {
|
||||
// String title
|
||||
test('to test the property `title`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// List<AssetResponseDto> assets (default value: const [])
|
||||
test('to test the property `assets`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -242,13 +242,13 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.3+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -333,10 +333,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
||||
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.0.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -367,7 +367,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3"
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.60.0+83
|
||||
version: 1.62.0+85
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
|
||||
environment:
|
||||
@@ -17,6 +17,7 @@ dependencies:
|
||||
flutter_hooks: ^0.18.6
|
||||
hooks_riverpod: ^2.2.0
|
||||
cached_network_image: ^3.2.2
|
||||
flutter_cache_manager: ^3.3.0
|
||||
intl: ^0.18.0
|
||||
auto_route: ^5.0.1
|
||||
fluttertoast: ^8.0.8
|
||||
@@ -45,6 +46,7 @@ dependencies:
|
||||
isar_flutter_libs: *isar_version # contains Isar Core
|
||||
permission_handler: ^10.2.0
|
||||
device_info_plus: ^8.1.0
|
||||
crypto: ^3.0.3 # TODO remove once native crypto is used on iOS
|
||||
|
||||
openapi:
|
||||
path: openapi
|
||||
|
||||
@@ -13,8 +13,8 @@ void main() {
|
||||
|
||||
testAssets.add(
|
||||
Asset(
|
||||
checksum: "",
|
||||
localId: '$i',
|
||||
deviceId: 1,
|
||||
ownerId: 1,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
@@ -23,7 +23,6 @@ void main() {
|
||||
type: AssetType.image,
|
||||
fileName: '',
|
||||
isFavorite: false,
|
||||
isLocal: false,
|
||||
isArchived: false,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -43,7 +43,12 @@ void main() {
|
||||
|
||||
test('withKey', () {
|
||||
final a = ["a", "bb", "cc", "ddd"];
|
||||
expect(a.uniqueConsecutive((s) => s.length), ["a", "bb", "ddd"]);
|
||||
expect(
|
||||
a.uniqueConsecutive(
|
||||
compare: (s1, s2) => s1.length.compareTo(s2.length),
|
||||
),
|
||||
["a", "bb", "ddd"],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,32 +6,33 @@ import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
void main() {
|
||||
Asset makeAsset({
|
||||
required String localId,
|
||||
required String checksum,
|
||||
String? localId,
|
||||
String? remoteId,
|
||||
int deviceId = 1,
|
||||
int ownerId = 590700560494856554, // hash of "1"
|
||||
bool isLocal = false,
|
||||
}) {
|
||||
final DateTime date = DateTime(2000);
|
||||
return Asset(
|
||||
checksum: checksum,
|
||||
localId: localId,
|
||||
remoteId: remoteId,
|
||||
deviceId: deviceId,
|
||||
ownerId: ownerId,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
updatedAt: date,
|
||||
durationInSeconds: 0,
|
||||
type: AssetType.image,
|
||||
fileName: localId,
|
||||
fileName: localId ?? remoteId ?? "",
|
||||
isFavorite: false,
|
||||
isLocal: isLocal,
|
||||
isArchived: false,
|
||||
);
|
||||
}
|
||||
@@ -53,6 +54,7 @@ void main() {
|
||||
|
||||
group('Test SyncService grouped', () {
|
||||
late final Isar db;
|
||||
final MockHashService hs = MockHashService();
|
||||
final owner = User(
|
||||
id: "1",
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -71,11 +73,11 @@ void main() {
|
||||
await Store.put(StoreKey.currentUser, owner);
|
||||
});
|
||||
final List<Asset> initialAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "1-1", isLocal: true),
|
||||
makeAsset(localId: "2", isLocal: true),
|
||||
makeAsset(localId: "3", isLocal: true),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "c", localId: "1", remoteId: "1-1"),
|
||||
makeAsset(checksum: "d", localId: "2"),
|
||||
makeAsset(checksum: "e", localId: "3"),
|
||||
];
|
||||
setUp(() {
|
||||
db.writeTxnSync(() {
|
||||
@@ -84,11 +86,11 @@ void main() {
|
||||
});
|
||||
});
|
||||
test('test inserting existing assets', () async {
|
||||
SyncService s = SyncService(db);
|
||||
SyncService s = SyncService(db, hs);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "1-1"),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "c", remoteId: "1-1"),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
@@ -97,14 +99,14 @@ void main() {
|
||||
});
|
||||
|
||||
test('test inserting new assets', () async {
|
||||
SyncService s = SyncService(db);
|
||||
SyncService s = SyncService(db, hs);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "1-1"),
|
||||
makeAsset(localId: "2", remoteId: "1-2"),
|
||||
makeAsset(localId: "4", remoteId: "1-4"),
|
||||
makeAsset(localId: "1", remoteId: "3-1", deviceId: 3),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "c", remoteId: "1-1"),
|
||||
makeAsset(checksum: "d", remoteId: "1-2"),
|
||||
makeAsset(checksum: "f", remoteId: "1-4"),
|
||||
makeAsset(checksum: "g", remoteId: "3-1", deviceId: 3),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
@@ -113,14 +115,14 @@ void main() {
|
||||
});
|
||||
|
||||
test('test syncing duplicate assets', () async {
|
||||
SyncService s = SyncService(db);
|
||||
SyncService s = SyncService(db, hs);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "1-1"),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "2-1b", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "2-1c", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "2-1d", deviceId: 2),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "1-1"),
|
||||
makeAsset(checksum: "c", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "h", remoteId: "2-1b", deviceId: 2),
|
||||
makeAsset(checksum: "i", remoteId: "2-1c", deviceId: 2),
|
||||
makeAsset(checksum: "j", remoteId: "2-1d", deviceId: 2),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
@@ -133,11 +135,13 @@ void main() {
|
||||
final bool c3 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
expect(c3, true);
|
||||
expect(db.assets.countSync(), 7);
|
||||
remoteAssets.add(makeAsset(localId: "1", remoteId: "2-1e", deviceId: 2));
|
||||
remoteAssets.add(makeAsset(localId: "2", remoteId: "2-2", deviceId: 2));
|
||||
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e", deviceId: 2));
|
||||
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2", deviceId: 2));
|
||||
final bool c4 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
expect(c4, true);
|
||||
expect(db.assets.countSync(), 9);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockHashService extends Mock implements HashService {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/nginxinc/nginx-unprivileged:1.23
|
||||
FROM ghcr.io/nginxinc/nginx-unprivileged:1.25.0-alpine3.17@sha256:e57300e9f60e521c5af3ec8fdc710285a371647e8033bcb8a36020c4394db3e3
|
||||
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
19
renovate.json
Normal file
19
renovate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPaths": ["mobile"],
|
||||
"groupName": "mobile"
|
||||
},
|
||||
{
|
||||
"matchPaths": ["server"],
|
||||
"groupName": "server"
|
||||
},
|
||||
{
|
||||
"matchPaths": ["web"],
|
||||
"groupName": "web"
|
||||
}
|
||||
],
|
||||
"enabled": false
|
||||
}
|
||||
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@@ -40,3 +40,5 @@ upload/
|
||||
tmp/
|
||||
core
|
||||
.reverse-geocoding-dump/
|
||||
|
||||
**/node_modules/**
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"semi": true
|
||||
"semi": true,
|
||||
"organizeImportsSkipDestructiveCodeActions": true
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
FROM node:18-alpine3.17 as builder
|
||||
FROM node:18.16.0-alpine3.18@sha256:f41850f74ff16a33daff988e2ea06ef8f5daeb6fb84913c7df09552a98caba09 as builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --update-cache build-base python3 vips-heif vips-dev ffmpeg perl
|
||||
RUN apk add --update-cache build-base imagemagick-dev python3 ffmpeg libraw-dev perl vips-dev vips-heif vips-magick
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
@@ -17,13 +17,13 @@ RUN npm run build
|
||||
RUN npm prune --omit=dev --omit=optional
|
||||
|
||||
|
||||
FROM node:18-alpine3.17
|
||||
FROM node:18.16.0-alpine3.18@sha256:f41850f74ff16a33daff988e2ea06ef8f5daeb6fb84913c7df09552a98caba09
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --no-cache vips-heif vips vips-cpp ffmpeg perl
|
||||
RUN apk add --no-cache ffmpeg imagemagick-dev libraw-dev perl vips-dev vips-heif vips-magick
|
||||
|
||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=prod /usr/src/app/dist ./dist
|
||||
@@ -32,7 +32,7 @@ COPY --from=prod /usr/src/app/bin ./bin
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
COPY package.json package-lock.json ./
|
||||
COPY start-server.sh start-microservices.sh ./
|
||||
COPY start*.sh ./
|
||||
|
||||
RUN npm link && npm cache clean --force
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
## How to run migration
|
||||
|
||||
1. Attached to the container shell
|
||||
2. Run `npm run typeorm -- migration:generate ./libs/database/src/<migration-name> -d libs/database/src/config/database.config.ts`
|
||||
3. Check if the migration file makes sense
|
||||
4. Move the migration file to folder `server/libs/database/src/migrations` in your code editor.
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"outDir": "../../dist/apps/cli"
|
||||
},
|
||||
"include": ["src/**/*", "../../libs/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AlbumCountResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
owned!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
shared!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
sharing!: number;
|
||||
|
||||
constructor(owned: number, shared: number, sharing: number) {
|
||||
this.owned = owned;
|
||||
this.shared = shared;
|
||||
this.sharing = sharing;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/* */ import { AlbumService, AuthUserDto, CreateAlbumDto, UpdateAlbumDto } from '@app/domain';
|
||||
import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||
|
||||
@ApiTags('Album')
|
||||
@Controller('album')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
export class AlbumController {
|
||||
constructor(private service: AlbumService) {}
|
||||
|
||||
@Get()
|
||||
getAllAlbums(@GetAuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto) {
|
||||
return this.service.getAll(authUser, query);
|
||||
}
|
||||
|
||||
@Post()
|
||||
createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumDto) {
|
||||
return this.service.create(authUser, dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
updateAlbumInfo(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) {
|
||||
return this.service.update(authUser, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
deleteAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.delete(authUser, id);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { AssetService, AuthUserDto, MapMarkerResponseDto } from '@app/domain';
|
||||
import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller('asset')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
export class AssetController {
|
||||
constructor(private service: AssetService) {}
|
||||
|
||||
@Get('/map-marker')
|
||||
getMapMarkers(@GetAuthUser() authUser: AuthUserDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||
return this.service.getMapMarkers(authUser, options);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"modulePaths": ["<rootDir>", "<rootDir>../../../"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"^@app/infra(|/.*)$": "<rootDir>../../../libs/infra/src/$1",
|
||||
"^@app/domain(|/.*)$": "<rootDir>../../../libs/domain/src/$1"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"outDir": "../../dist/apps/immich"
|
||||
},
|
||||
"include": ["src/**/*", "../../libs/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { getLogLevels, SERVER_VERSION } from '@app/domain';
|
||||
import { RedisIoAdapter } from '@app/infra';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppService } from './app.service';
|
||||
import { MicroservicesModule } from './microservices.module';
|
||||
import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
|
||||
|
||||
const logger = new Logger('ImmichMicroservice');
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(MicroservicesModule, {
|
||||
logger: getLogLevels(),
|
||||
});
|
||||
|
||||
const listeningPort = Number(process.env.MICROSERVICES_PORT) || 3002;
|
||||
|
||||
await app.get(AppService).init();
|
||||
|
||||
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||
|
||||
const metadataService = app.get(MetadataExtractionProcessor);
|
||||
|
||||
process.on('uncaughtException', (error: Error | any) => {
|
||||
const isCsvError = error.code === 'CSV_RECORD_INCONSISTENT_FIELDS_LENGTH';
|
||||
if (!isCsvError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.warn('Geocoding csv parse error, trying again without cache...');
|
||||
metadataService.init(true);
|
||||
});
|
||||
|
||||
await metadataService.init();
|
||||
|
||||
await app.listen(listeningPort, () => {
|
||||
const envName = (process.env.NODE_ENV || 'development').toUpperCase();
|
||||
logger.log(
|
||||
`Running Immich Microservices in ${envName} environment - version ${SERVER_VERSION} - Listening on port: ${listeningPort}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { MicroservicesModule } from './../src/microservices.module';
|
||||
|
||||
describe('MicroservicesController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [MicroservicesModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"outDir": "../../dist/apps/microservices"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
node ./dist/apps/cli/apps/cli/src/immich "$@"
|
||||
node ./dist/main cli "$@"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AlbumResponseDto, AuthService, CreateAlbumDto, SharedLinkResponseDto, UserService } from '@app/domain';
|
||||
import { CreateAlbumShareLinkDto } from '@app/immich/api-v1/album/dto/create-album-shared-link.dto';
|
||||
import { AppModule } from '@app/immich/app.module';
|
||||
import { AuthUserDto } from '@app/immich/decorators/auth-user.decorator';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import request from 'supertest';
|
||||
import { clearDb, getAuthUser, authCustom } from './test-utils';
|
||||
import { CreateAlbumDto } from '@app/domain';
|
||||
import { CreateAlbumShareLinkDto } from '../src/api-v1/album/dto/create-album-shared-link.dto';
|
||||
import { AuthUserDto } from '../src/decorators/auth-user.decorator';
|
||||
import { AlbumResponseDto, AuthService, SharedLinkResponseDto, UserService } from '@app/domain';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { authCustom, clearDb, getAuthUser } from '../test/test-utils';
|
||||
|
||||
async function _createAlbum(app: INestApplication, data: CreateAlbumDto) {
|
||||
const res = await request(app.getHttpServer()).post('/album').send(data);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user