Compare commits

...

43 Commits

Author SHA1 Message Date
Alex The Bot
a1183f4b4b Version v1.56.2 2023-05-20 03:53:45 +00:00
Alex
84cfa38510 chore(ml): load models on start up (#2487)
* chore(ml): load models on start up

* Download correct model
2023-05-19 22:37:01 -05:00
Fynn Petersen-Frey
89edbcacfa chore(mobile): remove obsolete files (#2482) 2023-05-19 22:06:39 -05:00
Alex The Bot
c8e649f190 Version v1.56.1 2023-05-19 04:01:36 +00:00
Alex
790e43dd6e chore(server): Enhancement for query to get assets for each recognized person (#2475) 2023-05-18 22:59:57 -05:00
Alex
59f6b2ff2e [Localizely] Translations update (#2471) 2023-05-18 15:10:22 -05:00
Alex Tran
829defbf61 docs: update RAM requirement 2023-05-18 15:04:21 -05:00
martin
70a0f4ae48 chore: update to node 18 and alpine 3.17 (#2430)
* chore: update to node 18 and alpine 3.17

Signed-off-by: martin <martin.labat92@gmail.com>

* chore: fix sharp version

Signed-off-by: martin <martin.labat92@gmail.com>

* chore(server): use vips-dev

Signed-off-by: martin <martin.labat92@gmail.com>

* update checkDiskUsage

Signed-off-by: martin <martin.labat92@gmail.com>

* fix: use vips-heif instead of libheif

Signed-off-by: martin <martin.labat92@gmail.com>

* fix: use vips instead of vips-cpp

Signed-off-by: martin <martin.labat92@gmail.com>

* fix: ensure vips installation

Signed-off-by: martin <martin.labat92@gmail.com>

---------

Signed-off-by: martin <martin.labat92@gmail.com>
2023-05-18 10:56:33 -05:00
Michel Heusschen
c7c0ef6abc chore(web): switch to eslint-plugin-svelte package (#2467) 2023-05-18 10:43:09 -05:00
Jason Rasmussen
2fc8a0db92 docs: add new features (#2441)
* chore: add new features

* Add facial recognition to docs site

* Added partner sharing to the docs site

* update developer docs

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-18 09:48:43 -05:00
Alex
b50c621be8 chore: update changelog for android 2023-05-18 09:47:12 -05:00
Alex The Bot
126f5857c3 Version v1.56.0 2023-05-18 14:03:48 +00:00
Alex Tran
8b3e1764a8 fix(web): asset count z-index 2023-05-17 21:39:34 -05:00
Alex
b776461297 fix(web): unable to change person name (#2458)
* fix(web): unable to change person name

* name changed

* chore: strongly-typed dispatcher

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-05-17 21:13:54 -05:00
Jason Rasmussen
4a0052026f feat(web): improve page header + scrolling (#2453)
* fix: line to edge of screen

* refactor: user layout page
2023-05-17 14:45:16 -05:00
Alex
35c4887e4a fix return update asset with new update time (#2457) 2023-05-17 14:28:58 -05:00
Mark Monteiro
f5b87833f8 Add comment about Docker secrets to example.env (#2454)
Add a comment to indicate the support for Docker secrets added in https://github.com/immich-app/immich/pull/1254
2023-05-17 17:36:44 +00:00
Fynn Petersen-Frey
0dde76bbbc feat(mobile): lazy loading of assets (#2413) 2023-05-17 12:36:02 -05:00
Jason Rasmussen
93863b0629 feat: facial recognition (#2180) 2023-05-17 12:07:17 -05:00
Michel Heusschen
115a47d4c6 fix(web): layout spacing when zooming (#2452) 2023-05-17 10:44:15 -05:00
martin
308c63df16 fix(web): use correct favicon sizes (#2446)
* fix(web): use correct favicon sizes

Signed-off-by: martin <martin.labat92@gmail.com>

* fix: format

Signed-off-by: martin <martin.labat92@gmail.com>

---------

Signed-off-by: martin <martin.labat92@gmail.com>
2023-05-17 09:20:32 -05:00
Michel Heusschen
ab86d0a18d refactor(web): asset select actions (#2444)
* refactor(web): asset select actions

* remaining pages/components + data flow changes

* fix check
2023-05-16 09:13:20 -05:00
Jason Rasmussen
3ec74444b0 docs: remove roadmap link (#2442) 2023-05-15 18:25:46 +00:00
Michel Heusschen
1979c84ea8 chore(web): update eslint and prettier packages (#2437)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-05-15 17:58:35 +00:00
Trenton H
f4aefcb18b chore(ci): versions the Docker cleaning action to latest released (#2438) 2023-05-15 12:45:03 -05:00
Sergey Kondrikov
7f2fa23179 feat (server, web): Share with partner (#2388)
* feat(server, web): implement share with partner

* chore: regenerate api

* chore: regenerate api

* Pass userId to getAssetCountByTimeBucket and getAssetByTimeBucket

* chore: regenerate api

* Use AssetGrid to view partner's assets

* Remove disableNavBarActions flag

* Check access to buckets

* Apply suggestions from code review

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

* Remove exception rethrowing

* Simplify partner access check

* Create new PartnerController

* chore api:generate

* Use partnerApi

* Remove id from PartnerResponseDto

* Refactor PartnerEntity

* Rename args

* Remove duplicate code in getAll

* Create composite primary keys for partners table

* Move asset access check into PartnerCore

* Remove redundant getUserAssets call

* Remove unused getUserAssets method

* chore: regenerate api

* Simplify getAll

* Replace ?? with ||

* Simplify PartnerRepository.create

* Introduce PartnerIds interface

* Replace two database migrations with one

* Simplify getAll

* Change PartnerResponseDto to include UserResponseDto

* Move partner sharing endpoints to PartnerController

* Rename ShareController to SharedLinkController

* chore: regenerate api after rebase

* refactor: shared link remove return type

* refactor: return user response dto

* chore: regenerate open api

* refactor: partner getAll

* refactor: partner settings event typing

* chore: remove unused code

* refactor: add partners modal trigger

* refactor: update url for viewing partner photos

* feat: update partner sharing title

* refactor: rename service method names

* refactor: http exception logic to service, PartnerIds interface

* chore: regenerate open api

* test: coverage for domain code

* fix: addPartner => createPartner

* fix: missed rename

* refactor: more code cleanup

* chore: alphabetize settings order

* feat: stop sharing confirmation modal

* Enhance contrast of the email in dark mode

* Replace button with CircleIconButton

* Fix linter warning

* Fix date types for PartnerEntity

* Fix PartnerEntity creation

* Reset assetStore state

* Change layout of the partner's assets page

* Add bulk download action for partner's assets

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-05-15 12:30:53 -05:00
Michel Heusschen
4524aa0d06 refactor(web): use ImmichApi to create urls (#2435) 2023-05-13 21:52:29 -05:00
Michel Heusschen
15fa8250cb fix(web): profile image load (#2434) 2023-05-13 09:16:14 -05:00
Alex
4dff129949 feat(web): Replicate albums view for sharing view (#2433)
* replicate album view for sharing view

* Remove unused file

* fix test

* correct title
2023-05-13 09:05:30 -05:00
Alex
43951ec208 chore(mobile): Upgrade to Flutter 3.10 (#2429)
* update dependencies

* resolve dependency and update code for Flutter 3.10

* update github action flutter version

* update test version

* iOS deployment

* pump intl package

* list tile fix
2023-05-12 09:21:13 -05:00
Alex
f961acdf0c feat(web): album card hover styling (#2424)
* feat(web): album card hover styling

* feedback

* fix delete button not shown

* better color
2023-05-11 11:50:48 -05:00
Alex
2c7821e5e6 chore: update api (#2428) 2023-05-11 10:49:28 -05:00
Alex
d25ddfc46b chore: update screenshots for readme and docs (#2425)
* update screenshot

* better quality for docs
2023-05-10 23:33:32 -05:00
Alex
8cc9b08c06 chore(ml): use official pytorch channel (#2416) 2023-05-10 07:09:33 -05:00
Michel Heusschen
98b9d815a6 chore(web): update svelte related packages (#2419) 2023-05-10 04:58:53 -05:00
Jason Rasmussen
a808b9403e feat(web,server): logout all devices (#2415)
* feat: logout all devices

* chore: regenerate openapi

* chore: add test

* chore: logout vs log out
2023-05-09 14:34:17 -05:00
Jason Rasmussen
c956eee919 chore: fix backup dumper formatting (#2414) 2023-05-09 13:44:50 -05:00
Alex The Bot
aa97ca9ccf Version v1.55.1 2023-05-09 15:29:06 +00:00
faupau
98bb3de8da fix(web) small UI improvements (#2369)
* small changes in asset viewer navigation

* add conditional wrapper and scroll only content

* fix formatting

* update conditional wrapper

* remove emptz title attribute

* remove conditional-wrapper as it is not needed

* remove isTimeline

* fix map over sidebar

* fix overlap

* fix conflict

* revert z-index

* add relative z index

---------

Co-authored-by: faupau03 <paul.paffe@gmx.net>
2023-05-09 10:10:13 -05:00
Michel Heusschen
cd43edf074 feat(mobile): improve localization (#2405) 2023-05-09 08:58:27 -05:00
Michel Heusschen
dffd992304 fix(web): remove global style from map marker (#2408) 2023-05-09 08:57:17 -05:00
bo0tzz
f1b70e13a1 Revert "Bump local-reverse-geocoder to 0.15.2 / Fix Corrupted Reverse Geocoding CSV File (#2396)" (#2409)
This reverts commit b5a4aef829.
2023-05-09 08:56:31 -05:00
Alex Tran
d91247dc35 chore: post release 2023-05-08 22:27:55 -05:00
371 changed files with 14667 additions and 10088 deletions

View File

@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.7.3"
flutter-version: "3.10.0"
cache: true
- name: Create the Keystore

View File

@@ -38,11 +38,12 @@ jobs:
-
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@develop
uses: stumpylog/image-cleaner-action/ephemeral@v0.1.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
is_org: "true"
do_delete: "true"
package_name: "${{ matrix.primary-name }}"
scheme: "pull_request"
repo_name: "immich"
@@ -69,9 +70,10 @@ jobs:
-
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@develop
uses: stumpylog/image-cleaner-action/untagged@v0.1.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
do_delete: "true"
is_org: "true"
package_name: "${{ matrix.primary-name }}"

View File

@@ -22,8 +22,8 @@ jobs:
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.7.3'
channel: "stable"
flutter-version: "3.10.0"
- name: Install dependencies
run: dart pub get
@@ -32,4 +32,3 @@ jobs:
- name: Run dart analyze
run: dart analyze --fatal-infos
working-directory: ./mobile

View File

@@ -112,10 +112,10 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.7.3"
flutter-version: "3.10.0"
- name: Run tests
working-directory: ./mobile
run: flutter test
run: flutter test -j 1
generated-api-up-to-date:
name: Check generated files are up-to-date

View File

@@ -60,25 +60,30 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
# Features
| Features | Mobile | Web |
| ------------------------------------------- | ------ | --- |
| Upload and view videos and photos | Yes | Yes |
| Auto backup when the app is opened | Yes | N/A |
| Selective album(s) for backup | Yes | N/A |
| Download photos and videos to local device | Yes | Yes |
| Multi-user support | Yes | Yes |
| Album and Shared albums | Yes | Yes |
| Scrubbable/draggable scrollbar | Yes | Yes |
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes |
| Metadata view (EXIF, map) | Yes | Yes |
| Search by metadata, objects and CLIP | Yes | Yes |
| Administrative functions (user management) | N/A | Yes |
| Background backup | Yes | N/A |
| Virtual scroll | Yes | Yes |
| OAuth support | Yes | Yes |
| LivePhoto backup and playback | iOS | Yes |
| User-defined storage structure | Yes | Yes |
| Public Sharing | N/A | Yes |
| Features | Mobile | Web |
| -------------------------------------------- | ------ | --- |
| Upload and view videos and photos | Yes | Yes |
| Auto backup when the app is opened | Yes | N/A |
| Selective album(s) for backup | Yes | N/A |
| Download photos and videos to local device | Yes | Yes |
| Multi-user support | Yes | Yes |
| Album and Shared albums | Yes | Yes |
| Scrubbable/draggable scrollbar | Yes | Yes |
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes |
| Metadata view (EXIF, map) | Yes | Yes |
| Search by metadata, objects, faces, and CLIP | Yes | Yes |
| Administrative functions (user management) | No | Yes |
| Background backup | Yes | N/A |
| Virtual scroll | Yes | Yes |
| OAuth support | Yes | Yes |
| API Keys | N/A | Yes |
| LivePhoto backup and playback | iOS | Yes |
| User-defined storage structure | Yes | Yes |
| Public Sharing | No | Yes |
| Archive and Favorites | Yes | Yes |
| Global Map | No | Yes |
| Partner Sharing | No | Yes |
| Facial recognition and clustering | No | Yes |
# Support the project

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -2,6 +2,8 @@
# Database
###################################################################################
# NOTE: The following four database variables support Docker secrets by adding a *_FILE suffix to the variable name
# See the docker-compose documentation on secrets for additional details: https://docs.docker.com/compose/compose-file/compose-file-v3/#secrets
DB_HOSTNAME=immich_postgres
DB_USERNAME=postgres
DB_PASSWORD=postgres

View File

@@ -18,21 +18,9 @@ gunzip < /path/to/backup/dump.sql.gz | docker exec -i immich_postgres psql -U po
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:
```
container_name: immich_postgres
image: postgres:14
env_file:
- .env
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
PGDATA: /var/lib/postgresql/data
volumes:
- pgdata:/var/lib/postgresql/data
- ./db_dumps:/db_dumps
restart: always
```yaml
services:
...
backup:
container_name: immich_db_dumper
image: prodrigestivill/postgres-backup-local

View File

@@ -111,7 +111,7 @@ After making any changes in the `server/libs/database/src/entities`, a database
2. Run
```bash
npm run typeorm -- migration:generate ./libs/infra/src/db/<migration-name> -d ./libs/infra/src/db/config/database.config.ts
npm run typeorm:migrations:generate ./libs/infra/src/<migration-name>
```
3. Check if the migration file makes sense.

View File

@@ -0,0 +1,15 @@
# Facial Recognition
Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them.
The list of people is shown in the Explore page.
<img src={require('./img/facial-recognition-1.png').default} title='Facial Recognition 1' />
Upon clicking on a person, a list of assets that contain their face will be shown.
<img src={require('./img/facial-recognition-2.png').default} title='Facial Recognition 2' />
The asset detail view will also show the faces that are recognized in the asset.
<img src={require('./img/facial-recognition-3.png').default} title='Facial Recognition 3' />

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View File

@@ -0,0 +1,17 @@
# Partner Sharing
Immich allows you to share your library with other users. They can then view your library and download the assets.
You can manage one or multiple users to have access to your library from the [User Settings](docs/features/user-settings.md) page.
<img src={require('./img/partner-sharing-1.png').default} title='Partner Sharing 1' />
<img src={require('./img/partner-sharing-2.png').default} title='Partner Sharing 2' />
Accessing the shared library can be done from the Sharing page.
<img src={require('./img/partner-sharing-3.png').default} title='Partner Sharing 3' />
:::tip Sharing specific assets
For sharing a specific set of assets, you can use the shared album feature of Immich.
:::

View File

@@ -18,5 +18,5 @@ You can also use Podman to run the application. However, additional configuratio
## Hardware
- **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS, etc). Windows works too, with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/)
- **RAM**: At least 2GB, preferred 4GB.
- **RAM**: At least 4GB, preferred 6GB.
- **CPU**: At least 2 cores, preferred 4 cores.

View File

@@ -110,11 +110,6 @@ const config = {
label: 'GitHub',
position: 'right',
},
{
href: 'https://github.com/orgs/immich-app/projects/1',
label: 'Roadmap',
position: 'right',
},
],
},
footer: {
@@ -153,10 +148,6 @@ const config = {
label: 'GitHub',
href: 'https://github.com/immich-app/immich',
},
{
label: 'Roadmap',
href: 'https://github.com/orgs/immich-app/projects/1',
},
],
},
],

View File

@@ -31,7 +31,7 @@ function HomepageHeader() {
</Link>
</div>
<img src="/img/immich-screenshots.webp" alt="logo" />
<img src="/img/immich-screenshots.png" alt="logo" />
</section>
</header>
);

BIN
docs/static/img/immich-screenshots.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -1 +1,3 @@
venv/
venv/
*.zip
*.onnx

View File

@@ -3,4 +3,170 @@
upload/
venv/
__pycache__/
model-cache/
model-cache/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
*.onnx
*.zip

View File

@@ -5,10 +5,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=true
RUN python -m venv /opt/venv
RUN /opt/venv/bin/pip install --pre torch -f https://download.pytorch.org/whl/nightly/cpu/torch_nightly.html
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
FROM python:3.10-slim

View File

@@ -1,9 +1,13 @@
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, util
from sentence_transformers import SentenceTransformer
from PIL import Image
from fastapi import FastAPI
import uvicorn
import os
from pydantic import BaseModel
@@ -15,24 +19,31 @@ class ClipRequestBody(BaseModel):
text: str
is_dev = os.getenv('NODE_ENV') == 'development'
server_port = os.getenv('MACHINE_LEARNING_PORT', 3003)
server_host = os.getenv('MACHINE_LEARNING_HOST', '0.0.0.0')
classification_model = os.getenv(
"MACHINE_LEARNING_CLASSIFICATION_MODEL", "microsoft/resnet-50"
)
object_model = os.getenv("MACHINE_LEARNING_OBJECT_MODEL", "hustvl/yolos-tiny")
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"
)
cache_folder = os.getenv("MACHINE_LEARNING_CACHE_FOLDER", "/cache")
_model_cache = {}
app = FastAPI()
"""
Model Initialization
"""
classification_model = os.getenv(
'MACHINE_LEARNING_CLASSIFICATION_MODEL', 'microsoft/resnet-50')
object_model = os.getenv('MACHINE_LEARNING_OBJECT_MODEL', 'hustvl/yolos-tiny')
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')
_model_cache = {}
@app.on_event("startup")
async def startup_event():
# Get all models
_get_model(object_model, "object-detection")
_get_model(classification_model, "image-classification")
_get_model(clip_image_model)
_get_model(clip_text_model)
_get_model(facial_recognition_model, "facial-recognition")
@app.get("/")
@@ -47,14 +58,14 @@ def ping():
@app.post("/object-detection/detect-object", status_code=200)
def object_detection(payload: MlRequestBody):
model = _get_model(object_model, 'object-detection')
model = _get_model(object_model, "object-detection")
assetPath = payload.thumbnailPath
return run_engine(model, assetPath)
@app.post("/image-classifier/tag-image", status_code=200)
def image_classification(payload: MlRequestBody):
model = _get_model(classification_model, 'image-classification')
model = _get_model(classification_model, "image-classification")
assetPath = payload.thumbnailPath
return run_engine(model, assetPath)
@@ -73,16 +84,47 @@ def clip_encode_text(payload: ClipRequestBody):
return model.encode(text).tolist()
@app.post("/facial-recognition/detect-faces", status_code=200)
def facial_recognition(payload: MlRequestBody):
model = _get_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 < 0.7:
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'] > 0.9):
tags = pred["label"].split(", ")
if pred["score"] > 0.9:
result = [*result, *tags]
if (len(result) > 1):
if len(result) > 1:
result = list(set(result))
return result
@@ -90,15 +132,27 @@ def run_engine(engine, path):
def _get_model(model, task=None):
global _model_cache
key = '|'.join([model, str(task)])
key = "|".join([model, str(task)])
if key not in _model_cache:
if task:
_model_cache[key] = pipeline(model=model, task=task)
if task == "facial-recognition":
face_model = FaceAnalysis(
name=model,
root=cache_folder,
allowed_modules=["detection", "recognition"],
)
face_model.prepare(ctx_id=0, det_size=(640, 640))
_model_cache[key] = face_model
else:
_model_cache[key] = pipeline(model=model, task=task)
else:
_model_cache[key] = SentenceTransformer(model)
_model_cache[key] = SentenceTransformer(model, cache_folder=cache_folder)
return _model_cache[key]
if __name__ == "__main__":
uvicorn.run("main:app", host=server_host,
port=int(server_port), reload=is_dev, workers=1)
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)

View File

@@ -1,4 +1,4 @@
{
"flutterSdkVersion": "3.7.0",
"flutterSdkVersion": "3.10.0",
"flavors": {}
}
}

View File

@@ -30,6 +30,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 78,
"android.injected.version.name" => "1.55.0",
"android.injected.version.code" => 79,
"android.injected.version.name" => "1.56.2",
}
)
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')

View File

@@ -0,0 +1 @@
* Minor UI improvement

View File

@@ -0,0 +1,2 @@
* Upgrade to Flutter 3.10
* Lazy loading of timeline

View File

@@ -5,17 +5,19 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000294">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00032">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="75.683384">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="29.247439">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="24.839722">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="22.794249">
<failure message="/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:42:in `block (2 levels) in parsing_binding&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/gems/fastlane-2.212.2/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/bin/fastlane:25:in `load&apos;&#10;/usr/local/Cellar/fastlane/2.212.2/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Google Api Error: Invalid request - APK specifies a version code that has already been used." />
</testcase>

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Přidáno do {album}",
"add_to_album_bottom_sheet_already_exists": "Již v {album}",
"advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení",
"advanced_settings_tile_title": "Pokročilé",
"advanced_settings_troubleshooting_subtitle": "Povolit dodatečné funkce pro řešení problémů",
"advanced_settings_troubleshooting_title": "Řešení problémů",
"album_info_card_backup_album_excluded": "VYLOUČENO",
"album_info_card_backup_album_included": "ZAHRNUTO",
"album_thumbnail_card_item": "1 položka",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Odstranit z alba",
"album_viewer_page_share_add_users": "Přidat uživatele",
"all_videos_page_title": "Videa",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archív ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamické rozložení",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Seskupit položky podle",
"asset_list_layout_settings_group_by_month": "Měsíc",
"asset_list_layout_settings_group_by_month_day": "Měsíc + den",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Přidat do alba",
"control_bottom_app_bar_album_info": "{} položek",
"control_bottom_app_bar_album_info_shared": "{} položky - sdílené",
"control_bottom_app_bar_archive": "Archív",
"control_bottom_app_bar_create_new_album": "Vytvořit nové album",
"control_bottom_app_bar_delete": "Vymazat",
"control_bottom_app_bar_favorite": "Oblíbené",
"control_bottom_app_bar_share": "Sdílet",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Bez názvu",
"create_shared_album_page_create": "Vytvořit",
"create_shared_album_page_share": "Sdílet",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Zrušit",
"delete_dialog_ok": "Vymazat",
"delete_dialog_title": "Vymazat trvale",
"description_input_hint_text": "Přidat popis...",
"description_input_submit_error": "Chyba aktualizace popisu, další podrobnosti najdete v logu",
"exif_bottom_sheet_description": "Přidat popis...",
"exif_bottom_sheet_details": "PODROBNOSTI",
"exif_bottom_sheet_location": "LOKALITA",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií",
"experimental_settings_subtitle": "Používejte na vlastní riziko!",
"experimental_settings_title": "Experimentální",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Oblíbené",
"home_page_add_to_album_conflicts": "Přidáno {added} položek do alba {album}. {failed} položek již je v albu.",
"home_page_add_to_album_err_local": "Zatím není možné přidat lokální média do alb, přeskakuje se",
"home_page_add_to_album_success": "Přidány položky {added} do alba {album}.",
"home_page_archive_err_local": "Zatím nemohu archivovat lokální média, přeskakuji",
"home_page_building_timeline": "Vytváření časové osy",
"home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuje se",
"home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných albech.",
"image_viewer_page_state_provider_download_error": "Chyba stahování",
"image_viewer_page_state_provider_download_success": "Stahování bylo úspěšné",
"library_page_albums": "Alba",
"library_page_archive": "Archív",
"library_page_device_albums": "Alba v zařízení",
"library_page_favorites": "Oblíbené",
"library_page_new_album": "Nové album",
@@ -206,7 +220,7 @@
"search_page_view_all_button": "Zobrazit vše",
"search_page_your_activity": "Vaše aktivita",
"search_result_page_new_search_hint": "Nové vyhledávání",
"search_suggestion_list_smart_search_hint_1": "Ve výchozím nastavení je chytré vyhledávání zapnuto, pro vyhledávání metadat použijte syntaxi",
"search_suggestion_list_smart_search_hint_1": "Ve výchozím nastavení je chytré vyhledávání zapnuto, pro vyhledávání metadat použijte syntaxi ",
"search_suggestion_list_smart_search_hint_2": "m:vaše-vyhledávaná-fráze",
"select_additional_user_for_sharing_page_suggestions": "Návrhy",
"select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Tilføjet til {album}",
"add_to_album_bottom_sheet_already_exists": "Allerede i {album}",
"advanced_settings_tile_subtitle": "Avancerede brugerindstillinger",
"advanced_settings_tile_title": "Arkivér",
"advanced_settings_troubleshooting_subtitle": "Slå ekstra funktioner for fejlsøgning til",
"advanced_settings_troubleshooting_title": "Fejlsøgning",
"album_info_card_backup_album_excluded": "EKSKLUDERET",
"album_info_card_backup_album_included": "INKLUDERET",
"album_thumbnail_card_item": "1 genstand",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Fjern fra album",
"album_viewer_page_share_add_users": "Tilføj brugere",
"all_videos_page_title": "Videoer",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Arkivér ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Gruppér elementer pr. ",
"asset_list_layout_settings_group_by_month": "Måned",
"asset_list_layout_settings_group_by_month_day": "Måned + dag",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Tilføj til album",
"control_bottom_app_bar_album_info": "{} genstande",
"control_bottom_app_bar_album_info_shared": "{} genstande • Delt",
"control_bottom_app_bar_archive": "Arkiv",
"control_bottom_app_bar_create_new_album": "Opret nyt album",
"control_bottom_app_bar_delete": "Slet",
"control_bottom_app_bar_favorite": "Favorit",
"control_bottom_app_bar_share": "Del",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Uden titel",
"create_shared_album_page_create": "Opret",
"create_shared_album_page_share": "Del",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Annuller",
"delete_dialog_ok": "Slet",
"delete_dialog_title": "Slet permanent",
"description_input_hint_text": "Tilføj en beskrivelse...",
"description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer",
"exif_bottom_sheet_description": "Tilføj beskrivelse...",
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "LOKATION",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter",
"experimental_settings_subtitle": "Brug på eget ansvar!",
"experimental_settings_title": "Eksperimentelle",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favoritter",
"home_page_add_to_album_conflicts": "Tilføjede {added} elementer til album {album}. {failed} elementer er allerede i albummet.",
"home_page_add_to_album_err_local": "Kan endnu ikke tilføje lokale elementer til album. Springer over..",
"home_page_add_to_album_success": "Tilføjede {added} elementer til album {album}.",
"home_page_archive_err_local": "Kan ikke arkivere lokalt element endnu.. Springer over",
"home_page_building_timeline": "Bygger tidslinjen",
"home_page_favorite_err_local": "Kan endnu ikke gøre lokale elementer til favoritter. Springer over..",
"home_page_first_time_notice": "Hvis dette er din første gang i appen, bedes du vælge en backup af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
"image_viewer_page_state_provider_download_error": "Fejl ved download",
"image_viewer_page_state_provider_download_success": "Download succesfuld",
"library_page_albums": "Albummer",
"library_page_archive": "Arkiv",
"library_page_device_albums": "Albummer på enhed",
"library_page_favorites": "Favoritter",
"library_page_new_album": "Nyt album",

View File

@@ -1,13 +1,17 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt",
"add_to_album_bottom_sheet_already_exists": "Bereits in {album}",
"advanced_settings_tile_subtitle": "Erweiterte Benutzereinstellungen",
"advanced_settings_tile_title": "Sonstige",
"advanced_settings_troubleshooting_subtitle": "Aktiviere erweiterte Funktionen zur Fehlersuche",
"advanced_settings_troubleshooting_title": "Fehlersuche",
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
"album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"album_thumbnail_owned": "Owned",
"album_thumbnail_shared_by": "Shared by {}",
"album_thumbnail_shared_by": "Geteilt von {}",
"album_viewer_appbar_share_delete": "Album löschen",
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
@@ -17,12 +21,15 @@
"album_viewer_appbar_share_remove": "Entferne vom Album",
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
"all_videos_page_title": "Videos",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisches Layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Gruppiere Elemente nach",
"asset_list_layout_settings_group_by_month": "Monat",
"asset_list_layout_settings_group_by_month_day": "Monat + Tag",
"asset_list_settings_subtitle": "Einstellungen für das Fotogitter-Layout",
"asset_list_settings_title": "Fotogitter",
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
@@ -31,21 +38,21 @@
"backup_album_selection_page_total_assets": "Elemente",
"backup_all": "Alle",
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "Suche nach neuen assets…",
"backup_background_service_error_title": "Backup error",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_background_service_connection_failed_message": "Konnte keine Verbindung zum Server herstellen. Neuer Versuch...",
"backup_background_service_current_upload_notification": "Lädt {} hoch",
"backup_background_service_default_notification": "Suche nach neuen Elementen…",
"backup_background_service_error_title": "Fehler bei der Sicherung",
"backup_background_service_in_progress_notification": "Elemente werden gesichert...",
"backup_background_service_upload_failure_notification": "Konnte {} nicht hochladen",
"backup_controller_page_albums": "Gesicherte Alben",
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
"backup_controller_page_background_app_refresh_enable_button_text": "Go to settings",
"backup_controller_page_background_battery_info_link": "Show me how",
"backup_controller_page_background_app_refresh_disabled_content": "Aktiviere Hintergrundaktualisierungen in Einstellungen -> Allgemein -> Hintergrundaktualisierungen um Sicherungen im Hintergrund zu ermöglichen. ",
"backup_controller_page_background_app_refresh_disabled_title": "Hintergrundaktualisierungen sind deaktiviert.",
"backup_controller_page_background_app_refresh_enable_button_text": "Gehe zu Einstellungen",
"backup_controller_page_background_battery_info_link": "Zeige mir wie",
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Battery optimizations",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_battery_info_title": "Batterieoptimierungen",
"backup_controller_page_background_charging": "Nur während des Ladens",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
@@ -53,7 +60,7 @@
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_wifi": "Only on WiFi",
"backup_controller_page_background_wifi": "Nur im WLAN",
"backup_controller_page_backup": "Sicherung",
"backup_controller_page_backup_selected": "Ausgewählt: ",
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
@@ -83,42 +90,44 @@
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
"backup_info_card_assets": "Elemente",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "Clear cache",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
"cache_settings_clear_cache_button": "Zwischenspeicher löschen",
"cache_settings_clear_cache_button_title": "Löscht den Zwischenspeicher der App. Dies wird die Leistungsfähigkeit der App deutlich einschränken, bis der Zwischenspeicher wieder aufgebaut wurde.",
"cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "Full images",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Vorschaubilder",
"cache_settings_statistics_title": "Cache usage",
"cache_settings_statistics_title": "Zwischenspeicher Nutzung",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"cache_settings_title": "Zwischenspeicher Einstellungen",
"change_password_form_confirm_password": "Passwort bestätigen",
"change_password_form_description": "Hallo {firstName} {lastName}\n\nDas ist entweder das erste Mal dass du dich einloggst oder eine Anfrage zur Änderung deines Passwortes wurde gestellt. Bitte gebe das neue Passwort ein.",
"change_password_form_new_password": "Neues Passwort",
"change_password_form_password_mismatch": "Passwörter stimmen nicht überein",
"change_password_form_reenter_new_password": "Passwort erneut eingeben",
"common_add_to_album": "Zu Album hinzufügen",
"common_change_password": "Passwort ändern",
"common_create_new_album": "Erstelle ein neues Album",
"common_server_error": "Bitte überprüfe Deine Netzwerkverbindung und stelle sicher, dass die App und Server Versionen kompatibel sind.",
"common_shared": "Geteilt",
"control_bottom_app_bar_add_to_album": "Zu Album hinzufügen",
"control_bottom_app_bar_album_info": "{} Elemente",
"control_bottom_app_bar_album_info_shared": "{} Elemente · geteilt",
"control_bottom_app_bar_archive": "Archiv",
"control_bottom_app_bar_create_new_album": "Neues Album erstellen",
"control_bottom_app_bar_delete": "Löschen",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_favorite": "Favorit",
"control_bottom_app_bar_share": "Teilen",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Unbenannt",
"create_shared_album_page_create": "Erstellen",
"create_shared_album_page_share": "Teilen",
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
"create_shared_album_page_share_select_photos": "Fotos auswählen",
"curated_location_page_title": "Places",
"curated_object_page_title": "Things",
"curated_location_page_title": "Orte",
"curated_object_page_title": "Dinge",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E d. LLL y • hh:mm",
@@ -126,110 +135,115 @@
"delete_dialog_cancel": "Abbrechen",
"delete_dialog_ok": "Löschen",
"delete_dialog_title": "Für immer löschen",
"description_input_hint_text": "Beschreibung hinzufügen...",
"description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.",
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "STANDORT",
"experimental_settings_new_asset_list_subtitle": "In Arbeit",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_new_asset_list_title": "Experimentelle Fotogitter aktivieren",
"experimental_settings_subtitle": "Benutzung auf eigene Gefahr!",
"experimental_settings_title": "Experimentell",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favoriten",
"home_page_add_to_album_conflicts": "{added} Elemente zu {album} hinzugefügt. {failed} Elemente sind bereits vorhanden.",
"home_page_add_to_album_err_local": "Kann lokale Elemente noch nicht zu Alben hinzufügen, überspringe",
"home_page_add_to_album_success": "{added} Elemente zu {album} hinzugefügt.",
"home_page_archive_err_local": "Kann lokale Elemente nicht archvieren, überspringe",
"home_page_building_timeline": "Zeitachse wird erstellt.",
"home_page_favorite_err_local": "Kann lokale Elemente noch nicht favorisieren, überspringe",
"home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefüllt werden kann.",
"image_viewer_page_state_provider_download_error": "Fehler beim Herunterladen",
"image_viewer_page_state_provider_download_success": "Erfolgreich heruntergeladen",
"library_page_albums": "Alben",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favorites",
"library_page_archive": "Archiv",
"library_page_device_albums": "Alben auf dem Gerät.",
"library_page_favorites": "Favoriten",
"library_page_new_album": "Neues Album",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"library_page_sharing": "Teilen",
"library_page_sort_created": "Zuletzt erstellt",
"library_page_sort_title": "Albumtitel",
"login_form_api_exception": "API Fehler. Bitte die Serveradresse überprüfen und erneut versuchen.",
"login_form_button_text": "Anmelden",
"login_form_email_hint": "deine@email.de",
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
"login_form_endpoint_url": "Server URL",
"login_form_err_http": "Bitte gebe http:// oder https:// an",
"login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_invalid_url": "Ungültige URL",
"login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
"login_form_failed_get_oauth_server_config": "Fehler beim Login per OAuth, Server-URL überprüfen",
"login_form_failed_get_oauth_server_disable": "OAuth-Funktion nicht verfügbar auf diesem Server.",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"login_form_label_email": "E-Mail",
"login_form_label_password": "Passwort",
"login_form_next_button": "Next",
"login_form_next_button": "Weiter",
"login_form_password_hint": "password",
"login_form_save_login": "Angemeldet bleiben",
"login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.",
"login_form_server_empty": "Serveradresse eingeben.",
"login_form_server_error": "Konnte nicht mit Server verbinden.",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"permission_onboarding_continue_anyway": "Continue anyway",
"motion_photos_page_title": "Live Photos",
"notification_permission_dialog_cancel": "Abbrechen",
"notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\"",
"notification_permission_dialog_settings": "Einstellungen",
"notification_permission_list_tile_content": "Erlaube Berechtigung für Benachrichtigungen",
"notification_permission_list_tile_enable_button": "Aktiviere Benachrichtigungen",
"notification_permission_list_tile_title": "Benachrichtigungs-Berechtigung",
"permission_onboarding_continue_anyway": "Trotzdem fortfahren",
"permission_onboarding_get_started": "Get started",
"permission_onboarding_go_to_settings": "Go to settings",
"permission_onboarding_grant_permission": "Grant permission",
"permission_onboarding_log_out": "Log out",
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
"permission_onboarding_go_to_settings": "Gehe zu Einstellungen",
"permission_onboarding_grant_permission": "Berechtigung erteilen",
"permission_onboarding_log_out": "Abmelden",
"permission_onboarding_permission_denied": "Berechtigungen verweigert. Um Immich zu benutzen, Zugriff auf Fotos und Videos in Einstellungen erlauben.",
"permission_onboarding_permission_granted": "Berechtigung erteilt! Du bist startklar.",
"permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermöglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.",
"permission_onboarding_request": "Immich benötigt Berechtigung um auf deine Fotos und Videos zuzugreifen.",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
"profile_drawer_settings": "Einstellungen",
"profile_drawer_sign_out": "Abmelden",
"recently_added_page_title": "Recently Added",
"recently_added_page_title": "Zuletzt hinzugefügt",
"search_bar_hint": "Durchsuche deine Fotos",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",
"search_page_categories": "Kategorien",
"search_page_favorites": "Favoriten",
"search_page_motion_photos": "Live Photos",
"search_page_no_objects": "Keine Objektinformationen verfügbar",
"search_page_no_places": "Keine Informationen über Orte verfügbar",
"search_page_places": "Orte",
"search_page_recently_added": "Recently added",
"search_page_screenshots": "Screenshots",
"search_page_recently_added": "Zuletzt hinzugefügt",
"search_page_screenshots": "Bildschirmfotos",
"search_page_selfies": "Selfies",
"search_page_things": "Dinge",
"search_page_videos": "Videos",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_page_view_all_button": "Alle anzeigen",
"search_page_your_activity": "Deine Aktivität",
"search_result_page_new_search_hint": "Neue Suche",
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
"search_suggestion_list_smart_search_hint_1": "Intelligente Suche ist standardmäßig aktiviert; um nach Metadaten zu suchen Syntax benutzen",
"search_suggestion_list_smart_search_hint_2": "m:dein-suchbegriff",
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image",
"setting_image_viewer_help": "Der Detailviewer lädt zuerst die kleine Miniaturansicht, dann die Vorschau in mittlerer Größe (falls aktiviert) und schließlich das Original (falls aktiviert).",
"setting_image_viewer_original_subtitle": "Aktivieren, um das Originalbild in voller Auflösung (groß!) zu laden. Deaktivieren, um den Datenverbrauch zu reduzieren (sowohl im Netzwerk als auch im Gerätespeicher).",
"setting_image_viewer_original_title": "Original laden",
"setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur die Miniaturansicht zu verwenden.",
"setting_image_viewer_preview_title": "Vorschaubild laden",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_immediately": "immediately",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "never",
"setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_notify_hours": "{} Stunden",
"setting_notifications_notify_immediately": "sofort",
"setting_notifications_notify_minutes": "{} Minuten",
"setting_notifications_notify_never": "niemals",
"setting_notifications_notify_seconds": "{} Sekunden",
"setting_notifications_single_progress_subtitle": "Detaillierte Upload Informationen für jedes Element.",
"setting_notifications_single_progress_title": "Show background backup detail progress",
"setting_notifications_single_progress_title": "Zeige Hintergrund-Sicherungs Detailfortschritt",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_title": "Notifications",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_notifications_title": "Benachrichtigungen",
"setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)",
"setting_notifications_total_progress_title": "Zeige Hintergrundsicherungsfortschritt",
"setting_pages_app_bar_settings": "Einstellungen",
"settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.",
"share_add": "Hinzufügen",
@@ -247,8 +261,8 @@
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Suche",
"tab_controller_nav_sharing": "Teilen",
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"theme_setting_asset_list_storage_indicator_title": "Zeige Sicherungsstatus auf Miniaturbild",
"theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({})",
"theme_setting_dark_mode_switch": "Dunkler Modus",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_thumbnail_card_item": "1 item",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Remove from album",
"album_viewer_page_share_add_users": "Add users",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
@@ -108,11 +115,12 @@
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete": "Delete",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_share": "Share",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share": "Share",
@@ -127,6 +135,8 @@
"delete_dialog_cancel": "Cancel",
"delete_dialog_ok": "Delete",
"delete_dialog_title": "Delete Permanently",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
@@ -134,22 +144,23 @@
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favorites",
"library_page_new_album": "New album",
"library_page_sharing": "Sharing",
"library_page_archive": "Archive",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
@@ -265,12 +276,5 @@
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"description_input_submit_error": "Error updating description, check the log for more details",
"description_input_hint_text": "Add description...",
"archive_page_title": "Archive ({})"
}
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
}

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "EXCLUIDOS",
"album_info_card_backup_album_included": "INCLUIDOS",
"album_thumbnail_card_item": "1 item",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Eliminar del álbum ",
"album_viewer_page_share_add_users": "Añadir usuarios",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Disposición dinámica",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Agrupar recursos por",
"asset_list_layout_settings_group_by_month": "Mes",
"asset_list_layout_settings_group_by_month_day": "Mes + día",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Añadir al álbum",
"control_bottom_app_bar_album_info": "{} elementos",
"control_bottom_app_bar_album_info_shared": "{} elementos · Compartido",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Crear nuevo álbum",
"control_bottom_app_bar_delete": "Eliminar",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Share",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share": "Compartir",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Eliminar",
"delete_dialog_title": "Eliminar Permanentemente",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Añadir Descripción...",
"exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "LOCALZACIÓN",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favoritos",
"home_page_add_to_album_conflicts": "Añadidos {added} elementos al álbum {album}. {failed} elementos ya están añadidos.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Añadidos {added} elementos al álbum {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Construyendo la línea de tiempo",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albums",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favoritos",
"library_page_new_album": "New album",

View File

@@ -1,13 +1,17 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"add_to_album_bottom_sheet_added": "Lisätty albumiin {album}",
"add_to_album_bottom_sheet_already_exists": "Kohde on jo albumissa {album}",
"advanced_settings_tile_subtitle": "Edistyneen käyttäjän asetukset",
"advanced_settings_tile_title": "Edistyneet",
"advanced_settings_troubleshooting_subtitle": "Kytke vianetsinnän lisäominaisuudet päälle",
"advanced_settings_troubleshooting_title": "Vianetsintä",
"album_info_card_backup_album_excluded": "JÄTETTY POIS",
"album_info_card_backup_album_included": "SISÄLLYTETTY",
"album_thumbnail_card_item": "1 kohde",
"album_thumbnail_card_items": "{} kohdetta",
"album_thumbnail_card_shared": "Jaettu",
"album_thumbnail_owned": "Owned",
"album_thumbnail_shared_by": "Shared by {}",
"album_thumbnail_owned": "Omistettu",
"album_thumbnail_shared_by": "Jakanut {}",
"album_viewer_appbar_share_delete": "Poista albumi",
"album_viewer_appbar_share_err_delete": "Albumin poistaminen epäonnistui",
"album_viewer_appbar_share_err_leave": "Albumista poistuminen epäonnistui",
@@ -16,8 +20,11 @@
"album_viewer_appbar_share_leave": "Poistu albumista",
"album_viewer_appbar_share_remove": "Poista albumista",
"album_viewer_page_share_add_users": "Lisää käyttäjiä",
"all_videos_page_title": "Videos",
"all_videos_page_title": "Videot",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Arkisto ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynaaminen asetelma",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Ryhmittele",
"asset_list_layout_settings_group_by_month": "Kuukauden mukaan",
"asset_list_layout_settings_group_by_month_day": "Kuukauden ja päivän mukaan",
@@ -38,9 +45,9 @@
"backup_background_service_in_progress_notification": "Varmuuskopioidaan kohteita...",
"backup_background_service_upload_failure_notification": "Lähetys palvelimelle epäonnistui {}",
"backup_controller_page_albums": "Varmuuskopioi albumit",
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
"backup_controller_page_background_app_refresh_enable_button_text": "Go to settings",
"backup_controller_page_background_app_refresh_disabled_content": "Salli sovelluksen päivittäminen taustalla suorittaaksesi varmuuskopiointia taustalla: Asetukset > Yleiset > Appien päivitys taustalla",
"backup_controller_page_background_app_refresh_disabled_title": "Sovelluksen päivittäminen taustalla on pois päältä",
"backup_controller_page_background_app_refresh_enable_button_text": "Siirry asetuksiin",
"backup_controller_page_background_battery_info_link": "Näytä minulle miten",
"backup_controller_page_background_battery_info_message": "Kytke pois päältä kaikki Immichin taustatyöskentelyyn liittyvät akun optimoinnit, jotta varmistat taustavarmuuskopioinnin parhaan mahdollisen toiminnan.\n\nKoska tämä on laitekohtaista, tarkista tarvittavat toimet laitevalmistajan ohjeista.",
"backup_controller_page_background_battery_info_ok": "OK",
@@ -95,30 +102,32 @@
"cache_settings_subtitle": "Hallitse Immich-mobiilisovelluksen välimuistin käyttöä",
"cache_settings_thumbnail_size": "Esikatselukuvien välimuistin koko ({} kohdetta)",
"cache_settings_title": "Välimuistin asetukset",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
"common_shared": "Shared",
"change_password_form_confirm_password": "Vahvista salasana",
"change_password_form_description": "Hei {firstName} {lastName},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.",
"change_password_form_new_password": "Uusi salasana",
"change_password_form_password_mismatch": "Salasanat eivät täsmää",
"change_password_form_reenter_new_password": "Uusi salasana uudelleen",
"common_add_to_album": "Lisää albumiin",
"common_change_password": "Vaihda salasana",
"common_create_new_album": "Luo uusi albumi",
"common_server_error": "Tarkista internet-yhteytesi. Varmista että palvelin on saavutettavissa ja sovellus-/palvelinversiot ovat yhteensopivia.",
"common_shared": "Jaettu",
"control_bottom_app_bar_add_to_album": "Lisää albumiin",
"control_bottom_app_bar_album_info": "{} kohdetta",
"control_bottom_app_bar_album_info_shared": "{} kohdetta · Jaettu",
"control_bottom_app_bar_archive": "Arkistoi",
"control_bottom_app_bar_create_new_album": "Luo uusi albumi",
"control_bottom_app_bar_delete": "Poista",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_favorite": "Suosikki",
"control_bottom_app_bar_share": "Jaa",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Nimetön",
"create_shared_album_page_create": "Luo",
"create_shared_album_page_share": "Jaa",
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
"create_shared_album_page_share_select_photos": "Valitse kuvat",
"curated_location_page_title": "Places",
"curated_object_page_title": "Things",
"curated_location_page_title": "Paikat",
"curated_object_page_title": "Asiat",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Peruuta",
"delete_dialog_ok": "Poista",
"delete_dialog_title": "Poista pysyvästi",
"description_input_hint_text": "Lisää kuvaus...",
"description_input_submit_error": "Virhe kuvauksen päivittämisessä, tarkista lisätiedot lokista",
"exif_bottom_sheet_description": "Lisää kuvaus…",
"exif_bottom_sheet_details": "TIEDOT",
"exif_bottom_sheet_location": "SIJAINTI",
@@ -133,23 +144,26 @@
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
"experimental_settings_title": "Kokeellinen",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Suosikit",
"home_page_add_to_album_conflicts": "Lisätty {added} kohdetta albumiin {album}. {failed} kohdetta on jo albumissa.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_err_local": "Paikallisten kohteiden lisääminen albumeihin ei ole mahdollista, ohitetaan",
"home_page_add_to_album_success": "Lisätty {added} kohdetta albumiin {album}.",
"home_page_archive_err_local": "Paikallisten kohteiden arkistointi ei ole mahdollista, ohitetaan",
"home_page_building_timeline": "Rakennetaan aikajanaa",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_favorite_err_local": "Paikallisten kohteiden lisääminen suosikkeihin ei ole mahdollista, ohitetaan",
"home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"image_viewer_page_state_provider_download_error": "Lataus epäonnistui",
"image_viewer_page_state_provider_download_success": "Lataus onnistui",
"library_page_albums": "Albumit",
"library_page_device_albums": "Albums on Device",
"library_page_archive": "Arkisto",
"library_page_device_albums": "Laitteen albumit",
"library_page_favorites": "Suosikit",
"library_page_new_album": "Uusi albumi",
"library_page_sharing": "Jakaminen",
"library_page_sort_created": "Viimeisin luotu",
"library_page_sort_title": "Albumin otsikko",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"login_form_api_exception": "API-virhe. Tarkista palvelimen URL-osoite ja yritä uudelleen.",
"login_form_button_text": "Kirjaudu",
"login_form_email_hint": "sahkopostisi@esimerkki.fi",
"login_form_endpoint_hint": "http://palvelimesi-osoite:portti/api",
@@ -164,55 +178,55 @@
"login_form_failed_login": "Virhe kirjautumisessa. Tarkista palvelimen URL, sähköpostiosoite ja salasana.",
"login_form_label_email": "Sähköposti",
"login_form_label_password": "Salasana",
"login_form_next_button": "Next",
"login_form_next_button": "Seuraava",
"login_form_password_hint": "salasana",
"login_form_save_login": "Pysy kirjautuneena",
"login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.",
"login_form_server_empty": "Syötä palvelimen URL-osoite.",
"login_form_server_error": "Palvelimeen ei saatu yhteyttä.",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"permission_onboarding_continue_anyway": "Continue anyway",
"permission_onboarding_get_started": "Get started",
"permission_onboarding_go_to_settings": "Go to settings",
"permission_onboarding_grant_permission": "Grant permission",
"permission_onboarding_log_out": "Log out",
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
"motion_photos_page_title": "Liikekuvat",
"notification_permission_dialog_cancel": "Peruuta",
"notification_permission_dialog_content": "Ottaaksesi ilmoitukset käyttöön, siirry asetuksiin ja valitse 'salli'.",
"notification_permission_dialog_settings": "Asetukset",
"notification_permission_list_tile_content": "Myönnä käyttöoikeus ottaaksesi ilmoitukset käyttöön.",
"notification_permission_list_tile_enable_button": "Ota ilmoitukset käyttöön",
"notification_permission_list_tile_title": "Ilmoitusten käyttöoikeus",
"permission_onboarding_continue_anyway": "Jatka silti",
"permission_onboarding_get_started": "Aloittaminen",
"permission_onboarding_go_to_settings": "Siirry asetuksiin",
"permission_onboarding_grant_permission": "Käyttöoikeuden myöntäminen",
"permission_onboarding_log_out": "Kirjaudu ulos",
"permission_onboarding_permission_denied": "Kielletty käyttöoikeus. Käyttääksesi Immichiä, myönnä oikeus kuviin ja videoihin asetuksista.",
"permission_onboarding_permission_granted": "Käyttöoikeus myönnetty! Kaikki valmista.",
"permission_onboarding_permission_limited": "Rajoitettu käyttöoikeus. Salliaksesi Immichin varmuuskopioida ja hallita koko kuvakirjastoasi, myönnä oikeus kuviin ja videoihin asetuksista.",
"permission_onboarding_request": "Immich vaatii käyttöoikeuden kuvien ja videoiden käyttämiseen.",
"profile_drawer_app_logs": "Lokit",
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
"profile_drawer_settings": "Asetukset",
"profile_drawer_sign_out": "Kirjaudu ulos",
"recently_added_page_title": "Recently Added",
"recently_added_page_title": "Viimeksi lisätyt",
"search_bar_hint": "Etsi kuvia",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",
"search_page_categories": "Kategoriat",
"search_page_favorites": "Suosikit",
"search_page_motion_photos": "Liikekuvat",
"search_page_no_objects": "Objektitietoja ei ole saatavilla",
"search_page_no_places": "Paikkatietoja ei ole saatavilla",
"search_page_places": "Paikat",
"search_page_recently_added": "Recently added",
"search_page_screenshots": "Screenshots",
"search_page_selfies": "Selfies",
"search_page_recently_added": "Viimeksi lisätyt",
"search_page_screenshots": "Näyttökuvat",
"search_page_selfies": "Selfiet",
"search_page_things": "Asiat",
"search_page_videos": "Videos",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_page_videos": "Videot",
"search_page_view_all_button": "Näytä kaikki",
"search_page_your_activity": "Toimintasi",
"search_result_page_new_search_hint": "Uusi haku",
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
"search_suggestion_list_smart_search_hint_1": "Älykäs haku on oletuksena käytössä. Käytä metatietojen etsimiseen syntaksia",
"search_suggestion_list_smart_search_hint_2": "m:hakusana",
"select_additional_user_for_sharing_page_suggestions": "Ehdotukset",
"select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui",
"select_user_for_sharing_page_share_suggestions": "Ehdotukset",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"server_info_box_app_version": "Sovelluksen versio",
"server_info_box_server_version": "Palvelimen versio",
"setting_image_viewer_help": "Sovellus lataa ensin pienen esikatselukuvan, toisena keskitarkkuuksisen kuvan (jos käytössä) ja kolmantena alkuperäisen täysitarkkuuksisen kuvan (jos käytössä)",
"setting_image_viewer_original_subtitle": "Ota käyttöön ladataksesi alkuperäinen täysitarkkuuksinen kuva (suuri!). Poista käytöstä vähentääksesi datan käyttöä (sekä verkossa että laitteen välimuistissa).",
"setting_image_viewer_original_title": "Lataa alkuperäinen kuva",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Ajouté à {album}",
"add_to_album_bottom_sheet_already_exists": "Déjà dans {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "EXCLU",
"album_info_card_backup_album_included": "INCLUS",
"album_thumbnail_card_item": "1 élément",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Retirer de l'album",
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Grouper les éléments par",
"asset_list_layout_settings_group_by_month": "Mois",
"asset_list_layout_settings_group_by_month_day": "Mois + jour",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Ajouter à l'album",
"control_bottom_app_bar_album_info": "{} éléments",
"control_bottom_app_bar_album_info_shared": "{} éléments - Partagés",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Créer un nouvel album",
"control_bottom_app_bar_delete": "Supprimer",
"control_bottom_app_bar_favorite": "Favoris",
"control_bottom_app_bar_share": "Partager",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Sans titre",
"create_shared_album_page_create": "Créer",
"create_shared_album_page_share": "Partager",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Annuler",
"delete_dialog_ok": "Supprimer",
"delete_dialog_title": "Supprimer définitivement",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Ajouter une description...",
"exif_bottom_sheet_details": "DÉTAILS",
"exif_bottom_sheet_location": "LOCALISATION",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
"experimental_settings_subtitle": "Utilisez à vos dépends !",
"experimental_settings_title": "Expérimental",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favoris",
"home_page_add_to_album_conflicts": "{added} éléments ajoutés à l'album {album}. Les éléments {failed} sont déjà dans l'album.",
"home_page_add_to_album_err_local": "Impossible d'ajouter des éléments locaux aux albums pour le moment, étape ignorée",
"home_page_add_to_album_success": "{added} éléments ajoutés à l'album {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Construction de la chronologie",
"home_page_favorite_err_local": "Impossible d'ajouter des éléments locaux aux favoris pour le moment, étape ignorée",
"home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
"image_viewer_page_state_provider_download_error": "Erreur de téléchargement",
"image_viewer_page_state_provider_download_success": "Téléchargement réussi",
"library_page_albums": "Albums",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favoris",
"library_page_new_album": "Nouvel album",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Aggiunto in {album}",
"add_to_album_bottom_sheet_already_exists": "Già presente in {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "ESCLUSI",
"album_info_card_backup_album_included": "INCLUSI",
"album_thumbnail_card_item": "1 elemento ",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
"album_viewer_page_share_add_users": "Aggiungi utenti",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Layout dinamico",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Raggruppa le immagini per",
"asset_list_layout_settings_group_by_month": "Mese",
"asset_list_layout_settings_group_by_month_day": "Mese + giorno",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Aggiungi all'album",
"control_bottom_app_bar_album_info": "{} elementi",
"control_bottom_app_bar_album_info_shared": "{} elementi · Condivisi",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Crea nuovo album",
"control_bottom_app_bar_delete": "Elimina",
"control_bottom_app_bar_favorite": "Preferiti",
"control_bottom_app_bar_share": "Condividi",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Senza titolo",
"create_shared_album_page_create": "Crea",
"create_shared_album_page_share": "Condividi",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Annulla",
"delete_dialog_ok": "Elimina",
"delete_dialog_title": "Cancella definitivamente",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
"exif_bottom_sheet_details": "DETTAGLI",
"exif_bottom_sheet_location": "POSIZIONE",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Attiva griglia di foto sperimentale",
"experimental_settings_subtitle": "Usalo a tuo rischio!",
"experimental_settings_title": "Sperimentale",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Preferiti",
"home_page_add_to_album_conflicts": "Aggiunti {added} elementi all'album {album}. {failed} elementi erano già presenti nell'album.",
"home_page_add_to_album_err_local": "Non puoi aggiungere negli album foto ancora non caricate",
"home_page_add_to_album_success": "Aggiunti {added} elementi all'album {album}",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Costruendo il Timeline",
"home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti le foto ancora non caricate",
"home_page_first_time_notice": "Se è la prima volta che usi l'app, assicurati di scegliere gli album per avere il Timeline con immagini e video",
"image_viewer_page_state_provider_download_error": "Errore nel Download",
"image_viewer_page_state_provider_download_success": "Download con successo",
"library_page_albums": "Album",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Preferiti",
"library_page_new_album": "Nuovo Album",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "{album}に追加しました",
"add_to_album_bottom_sheet_already_exists": "{album}にもう存在してます",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "除外中",
"album_info_card_backup_album_included": "選択中",
"album_thumbnail_card_item": "項目数: 1",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "アルバムから除外",
"album_viewer_page_share_add_users": "ユーザーを追加",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "ダイナミックレイアウト",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "写真をグループ分けする方法:",
"asset_list_layout_settings_group_by_month": "月",
"asset_list_layout_settings_group_by_month_day": "月+日",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "アルバムに追加",
"control_bottom_app_bar_album_info": "{}枚の写真",
"control_bottom_app_bar_album_info_shared": "{}枚の共有中の写真",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "新しいアルバムを作成",
"control_bottom_app_bar_delete": "削除",
"control_bottom_app_bar_favorite": "お気に入り",
"control_bottom_app_bar_share": "共有",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "タイトル無し",
"create_shared_album_page_create": "作成",
"create_shared_album_page_share": "共有",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "キャンセル",
"delete_dialog_ok": "削除",
"delete_dialog_title": "永久的に削除",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "概要を追加",
"exif_bottom_sheet_details": "詳細な情報",
"exif_bottom_sheet_location": "撮影地",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "試験的なグリッドを有効",
"experimental_settings_subtitle": "試験的だから自己責任でね",
"experimental_settings_title": "試験的",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "お気に入り",
"home_page_add_to_album_conflicts": "{album}に{added}枚写真を追加しました。{failed}枚の写真は常に存在してたよ",
"home_page_add_to_album_err_local": "まだアップロードされてない写真はアルバムに登録できないよ",
"home_page_add_to_album_success": "{album}に{added}枚写真を追加しました",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "タイムラインを構築中",
"home_page_favorite_err_local": "まだアップロードされてない写真はお気に入り登録できないよ",
"home_page_first_time_notice": "アプリを使うのがはじめての場合タイムラインに写真を表示するためにアルバムを選択してね",
"image_viewer_page_state_provider_download_error": "ダウンロードエラー",
"image_viewer_page_state_provider_download_success": "ダウンロードできました",
"library_page_albums": "アルバム",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "お気に入り",
"library_page_new_album": "新しいアルバム",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "{album}에 추가",
"add_to_album_bottom_sheet_already_exists": "{album}에 이미 포함되어 있습니다",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "제외됨",
"album_info_card_backup_album_included": "포함됨",
"album_thumbnail_card_item": "1개 항목",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "앨범에서 제거",
"album_viewer_page_share_add_users": "사용자 추가",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "다이나믹 레이아웃",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "다음으로 그룹화",
"asset_list_layout_settings_group_by_month": "월",
"asset_list_layout_settings_group_by_month_day": "월 + 일",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "앨범에 추가",
"control_bottom_app_bar_album_info": "{} 항목",
"control_bottom_app_bar_album_info_shared": "{} 항목 · 공유됨",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "앨범 생성",
"control_bottom_app_bar_delete": "삭제",
"control_bottom_app_bar_favorite": "즐겨찾기",
"control_bottom_app_bar_share": "공유",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "제목없음",
"create_shared_album_page_create": "만들기",
"create_shared_album_page_share": "공유",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "취소",
"delete_dialog_ok": "삭제",
"delete_dialog_title": "영구적으로 삭제",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "설명 추가...",
"exif_bottom_sheet_details": "상세정보",
"exif_bottom_sheet_location": "위치",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "실험적 사진 그리드 적용",
"experimental_settings_subtitle": "문제시 책임지지 않습니다!",
"experimental_settings_title": "실험적기능",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "즐겨찾기",
"home_page_add_to_album_conflicts": "{album} 앨범에 {added} 미디어를 추가했습니다. {failed} 이미 앨범에 있는 항목입니다.",
"home_page_add_to_album_err_local": "앨범에 미디어파일을 추가할 수 없어, 건너뜁니다.",
"home_page_add_to_album_success": "{album} 앨범에 {added} 미디어를 추가했습니다. ",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "타임라인 생성",
"home_page_favorite_err_local": "미디어파일을 즐겨찾기에 추가할 수 없어, 건너뜁니다.",
"home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인이 앨범의 사진과 비디오를 채울 수 있도록 백업대상 앨범을 선택해야 합니다.",
"image_viewer_page_state_provider_download_error": "다운로드 에러",
"image_viewer_page_state_provider_download_success": "다운로드 완료",
"library_page_albums": "앨범",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "즐겨찾기",
"library_page_new_album": "새 앨범",

View File

@@ -1,13 +1,17 @@
{
"add_to_album_bottom_sheet_added": "Lagt til i {album}",
"add_to_album_bottom_sheet_already_exists": "Allerede i {album}",
"advanced_settings_tile_subtitle": "Avanserte brukerinnstillinger",
"advanced_settings_tile_title": "Avansert",
"advanced_settings_troubleshooting_subtitle": "Aktiver ekstra funksjoner for feilsøking",
"advanced_settings_troubleshooting_title": "Feilsøking",
"album_info_card_backup_album_excluded": "EKSKLUDERT",
"album_info_card_backup_album_included": "INKLUDERT",
"album_thumbnail_card_item": "1 objekt",
"album_thumbnail_card_items": "{} objekter",
"album_thumbnail_card_shared": "Delt",
"album_thumbnail_owned": "Owned",
"album_thumbnail_shared_by": "Shared by {}",
"album_thumbnail_owned": "Eid",
"album_thumbnail_shared_by": "Delt av {}",
"album_viewer_appbar_share_delete": "Slett album",
"album_viewer_appbar_share_err_delete": "Feilet ved sletting av album",
"album_viewer_appbar_share_err_leave": "Kunne ikke forlate albumet",
@@ -16,8 +20,11 @@
"album_viewer_appbar_share_leave": "Forlat album",
"album_viewer_appbar_share_remove": "Fjern fra album",
"album_viewer_page_share_add_users": "Legg til brukere",
"all_videos_page_title": "Videos",
"all_videos_page_title": "Videoer",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Arkiv ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Grupper bilder etter",
"asset_list_layout_settings_group_by_month": "Måned",
"asset_list_layout_settings_group_by_month_day": "Måned + dag",
@@ -103,22 +110,24 @@
"common_add_to_album": "Legg til i album",
"common_change_password": "Endre passord",
"common_create_new_album": "Lag nytt album",
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
"common_server_error": "Sjekk nettverkstilkobling, vær sikker på at serveren er mulig å nå og at app/server-versjoner er kompatible.",
"common_shared": "Delt",
"control_bottom_app_bar_add_to_album": "Legg til i album",
"control_bottom_app_bar_album_info": "{} objekter",
"control_bottom_app_bar_album_info_shared": "{} objekter · Delt",
"control_bottom_app_bar_archive": "Arkiver",
"control_bottom_app_bar_create_new_album": "Lag nytt album",
"control_bottom_app_bar_delete": "Slett",
"control_bottom_app_bar_favorite": "Favoritt",
"control_bottom_app_bar_share": "Del",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Uten navn",
"create_shared_album_page_create": "Opprett",
"create_shared_album_page_share": "Del",
"create_shared_album_page_share_add_assets": "LEGG TIL OBJEKTER",
"create_shared_album_page_share_select_photos": "Velg bilder",
"curated_location_page_title": "Places",
"curated_object_page_title": "Things",
"curated_location_page_title": "Plasseringer",
"curated_object_page_title": "Ting",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Avbryt",
"delete_dialog_ok": "Slett",
"delete_dialog_title": "Slett permanent",
"description_input_hint_text": "Legg til beskrivelse...",
"description_input_submit_error": "Feil ved oppdatering av beskrivelse, sjekk loggen for flere detaljer",
"exif_bottom_sheet_description": "Legg til beskrivelse...",
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "PLASSERING",
@@ -133,23 +144,26 @@
"experimental_settings_new_asset_list_title": "Aktiver eksperimentell grid-visning",
"experimental_settings_subtitle": "Bruk på egen risiko!",
"experimental_settings_title": "Eksperimentelt",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favoritter",
"home_page_add_to_album_conflicts": "Lagt til {added} objekter til album {album}. {failed} objekter er allerede i albumet.",
"home_page_add_to_album_err_local": "Kan ikke legge til lokale objekter til album enda, hopper over",
"home_page_add_to_album_success": "Lagt til {added} objekter til album {album}.",
"home_page_archive_err_local": "Kan ikke arkivere lokale objekter enda, hopper over",
"home_page_building_timeline": "Genererer tidslinjen",
"home_page_favorite_err_local": "Kan ikke sette favoritt på lokale objekter enda, hopper over",
"home_page_first_time_notice": "Hvis dette er første gangen du benytter appen, så velg ett album(eller flere) slik at tidslinjen kan genereres med dine bilder og videoer.",
"image_viewer_page_state_provider_download_error": "Nedlasting feilet",
"image_viewer_page_state_provider_download_success": "Nedlasting vellykket",
"library_page_albums": "Albumer",
"library_page_device_albums": "Albums on Device",
"library_page_archive": "Arkiv",
"library_page_device_albums": "Albumer på enheten",
"library_page_favorites": "Favoritter",
"library_page_new_album": "Nytt album",
"library_page_sharing": "Deling",
"library_page_sort_created": "Nylig opplastet",
"library_page_sort_title": "Album tittel",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"login_form_api_exception": "API feil. Sjekk server URL og prøv igjen.",
"login_form_button_text": "Logg inn",
"login_form_email_hint": "dinepost@epost.no",
"login_form_endpoint_hint": "http://din-server-ip:port/api",
@@ -164,59 +178,59 @@
"login_form_failed_login": "Feil ved innlogging, sjekk server URL, epost og passord",
"login_form_label_email": "Epostadresse",
"login_form_label_password": "Passord",
"login_form_next_button": "Next",
"login_form_next_button": "Neste",
"login_form_password_hint": "passord",
"login_form_save_login": "Forbli innlogget",
"login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.",
"login_form_server_empty": "Skriv inn en server URL.",
"login_form_server_error": "Kan ikke koble til server.",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"motion_photos_page_title": "Bevegelige bilder",
"notification_permission_dialog_cancel": "Avbryt",
"notification_permission_dialog_content": "For å aktivere notifikasjoner, gå til Innstillinger og velg tillat.",
"notification_permission_dialog_settings": "Innstillinger",
"notification_permission_list_tile_content": "Tillat tilgang for å aktivere notifikasjoner",
"notification_permission_list_tile_enable_button": "Aktiver notifikasjoner",
"notification_permission_list_tile_title": "Notifikasjonstilgang",
"permission_onboarding_continue_anyway": "Continue anyway",
"permission_onboarding_get_started": "Get started",
"permission_onboarding_go_to_settings": "Go to settings",
"permission_onboarding_grant_permission": "Grant permission",
"permission_onboarding_log_out": "Log out",
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
"permission_onboarding_continue_anyway": "Fortsett uansett",
"permission_onboarding_get_started": "Kom i gang",
"permission_onboarding_go_to_settings": "Gå til innstillinger",
"permission_onboarding_grant_permission": "Gi tilgang",
"permission_onboarding_log_out": "Logg ut",
"permission_onboarding_permission_denied": "Tilgang avvist. For å bruke Immich, tillat å vise bilde og videoer i Innstillinger.",
"permission_onboarding_permission_granted": "Tilgang gitt! Du er i gang.",
"permission_onboarding_permission_limited": "Tilgang begrenset. For å la Immich ta backup og håndtere galleriet, tillatt bilde og video-tilgang i Innstillinger.",
"permission_onboarding_request": "Immich trenger tilgang til å se dine bilder og videoer",
"profile_drawer_app_logs": "Logg",
"profile_drawer_client_server_up_to_date": "Klient og Server er oppdatert",
"profile_drawer_settings": "Innstillinger",
"profile_drawer_sign_out": "Logg ut",
"recently_added_page_title": "Recently Added",
"recently_added_page_title": "Nylig lagt til",
"search_bar_hint": "Søk i dine bilder",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",
"search_page_categories": "Kategorier",
"search_page_favorites": "Favoritter",
"search_page_motion_photos": "Bevegelige bilder",
"search_page_no_objects": "Ingen objektinfo tilgjengelig",
"search_page_no_places": "Ingen plasseringsinfo tilgjengelig",
"search_page_places": "Plasser",
"search_page_recently_added": "Recently added",
"search_page_screenshots": "Screenshots",
"search_page_selfies": "Selfies",
"search_page_recently_added": "Nylig lagt til",
"search_page_screenshots": "Skjermbilder",
"search_page_selfies": "Selfier",
"search_page_things": "Ting",
"search_page_videos": "Videos",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_page_videos": "Videoer",
"search_page_view_all_button": "Vis alle",
"search_page_your_activity": "Din aktivitet",
"search_result_page_new_search_hint": "Nytt søk",
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
"search_suggestion_list_smart_search_hint_1": "Smartsøk er aktivert som standard, for å søke etter metadata bruk syntaks ",
"search_suggestion_list_smart_search_hint_2": "m:ditt-søkeord",
"select_additional_user_for_sharing_page_suggestions": "Forslag",
"select_user_for_sharing_page_err_album": "Feilet ved oppretting av album",
"select_user_for_sharing_page_share_suggestions": "Forslag",
"server_info_box_app_version": "App versjon",
"server_info_box_server_version": "Server versjon",
"setting_image_viewer_help": "Først lastes mikrobilder, deretter middels-oppløsningbildet (hvis aktivert), til slutt lastes original (hvis aktivert).",
"setting_image_viewer_help": "Først lastes mikrobilder, deretter forhåndsvisningsbildet (hvis aktivert), til slutt lastes original (hvis aktivert).",
"setting_image_viewer_original_subtitle": "Aktiver for å laste originalbildet i full oppløsning (Stort!). Deaktiver for å spare databruk (både nettverksbruk og bufferdata på enheten).",
"setting_image_viewer_original_title": "Last originalbildet",
"setting_image_viewer_preview_subtitle": "Aktiver for å laste ett bilde av middels-oppløsning. Deaktiver for å enten direkte laste inn originalen eller kun benytte miniatyrbilde.",
"setting_image_viewer_preview_subtitle": "Aktiver for å laste ett bilde av medium oppløsning. Deaktiver for å enten direkte laste inn originalen eller kun benytte miniatyrbilde.",
"setting_image_viewer_preview_title": "Last forhåndsvisningsbilde",
"setting_notifications_notify_failures_grace_period": "Varsle om sikkerhetskopieringsfeil i bakgrunnen: {}",
"setting_notifications_notify_hours": "{} timer",
@@ -251,7 +265,7 @@
"theme_setting_asset_list_tiles_per_row_title": "Antall ressurser per rad ({})",
"theme_setting_dark_mode_switch": "Mørk modus",
"theme_setting_image_viewer_quality_subtitle": "Juster kvaliteten på detaljer med bildeviser",
"theme_setting_image_viewer_quality_title": "Bilderviser-kvalitet",
"theme_setting_image_viewer_quality_title": "Kvalitet på bildevisning",
"theme_setting_system_theme_switch": "Automatisk (følg system)",
"theme_setting_theme_subtitle": "Velg app'ens temainnstilling",
"theme_setting_theme_title": "Tema",
@@ -261,6 +275,6 @@
"version_announcement_overlay_release_notes": "Endringslogg",
"version_announcement_overlay_text_1": "Hei, det er en ny versjon av",
"version_announcement_overlay_text_2": "vennligst ta deg tid til å besøke",
"version_announcement_overlay_text_3": "og verifiser at din docker-compose og .env oppsett er oppdatert for å forhindre en eventuell miskonfigurasjon. Spesielt hvis du benytter WatchTower eller en annen tjeneste som håndterer oppdatering av applikasjoner på serveren automatisk.",
"version_announcement_overlay_text_3": " og verifiser at din docker-compose og .env oppsett er oppdatert for å forhindre en eventuell miskonfigurasjon. Spesielt hvis du benytter WatchTower eller en annen tjeneste som håndterer oppdatering av applikasjoner på serveren automatisk.",
"version_announcement_overlay_title": "Ny serverversjon tilgjengelig"
}

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Toegevoegd aan {album}",
"add_to_album_bottom_sheet_already_exists": "Staat al in {album}",
"advanced_settings_tile_subtitle": "Geavanceerde gebruikersinstellingen",
"advanced_settings_tile_title": "Geavanceerd",
"advanced_settings_troubleshooting_subtitle": "Schakel extra functies in voor probleemoplossing",
"advanced_settings_troubleshooting_title": "Probleemoplossing",
"album_info_card_backup_album_excluded": "UITGESLOTEN",
"album_info_card_backup_album_included": "INGESLOTEN",
"album_thumbnail_card_item": "1 item",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Verwijder uit album",
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
"all_videos_page_title": "Video's",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archief ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamische layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Groupeer items per",
"asset_list_layout_settings_group_by_month": "Maand",
"asset_list_layout_settings_group_by_month_day": "Maand + dag",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Toevoegen aan album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Gedeeld",
"control_bottom_app_bar_archive": "Archiveren",
"control_bottom_app_bar_create_new_album": "Maak nieuw album",
"control_bottom_app_bar_delete": "Verwijderen",
"control_bottom_app_bar_favorite": "Favoriet",
"control_bottom_app_bar_share": "Delen",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Naamloos",
"create_shared_album_page_create": "Aanmaken",
"create_shared_album_page_share": "Delen",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Annuleren",
"delete_dialog_ok": "Verwijderen",
"delete_dialog_title": "Permanent verwijderen",
"description_input_hint_text": "Beschrijving toevoegen...",
"description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details",
"exif_bottom_sheet_description": "Beschrijving toevoegen...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATIE",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Experimenteel foto grid inschakelen",
"experimental_settings_subtitle": "Gebruik op eigen risico!",
"experimental_settings_title": "Experimenteel",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favorieten",
"home_page_add_to_album_conflicts": "{added} items toegevoegd aan album {album}. {failed} items staan al in het album.",
"home_page_add_to_album_err_local": "Lokale items kunnen nog niet aan albums worden toegevoegd, overslaan",
"home_page_add_to_album_success": "{added} items toegevoegd aan album {album}.",
"home_page_archive_err_local": "Lokale items kunnen nog niet gearchiveerd worden, overslaan",
"home_page_building_timeline": "Tijdlijn opbouwen",
"home_page_favorite_err_local": "Lokale items kunnen nog niet als favoriet worden aangemerkt, overslaan",
"home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
"image_viewer_page_state_provider_download_error": "Download mislukt",
"image_viewer_page_state_provider_download_success": "Download succesvol",
"library_page_albums": "Albums",
"library_page_archive": "Archief",
"library_page_device_albums": "Albums op apparaat",
"library_page_favorites": "Favorieten",
"library_page_new_album": "Nieuw album",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "WYKLUCZONE",
"album_info_card_backup_album_included": "WŁĄCZONE",
"album_thumbnail_card_item": "1 pozycja",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Usuń z albumu",
"album_viewer_page_share_add_users": "Dodaj użytkowników",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete": "Usuń",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Udostępnij",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Bez tytułu",
"create_shared_album_page_create": "Utwórz",
"create_shared_album_page_share": "Udostępnij",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Anuluj",
"delete_dialog_ok": "Usuń",
"delete_dialog_title": "Usuń trwale",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Dodaj Opis...",
"exif_bottom_sheet_details": "SZCZEGÓŁY",
"exif_bottom_sheet_location": "LOKALIZACJA",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Albumy",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favorites",
"library_page_new_album": "Nowy album",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Добавлено в {album}",
"add_to_album_bottom_sheet_already_exists": "Уже в {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "ИСКЛЮЧЕН",
"album_info_card_backup_album_included": "ВКЛЮЧЕН",
"album_thumbnail_card_item": "1 объект",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Удалить из альбома",
"album_viewer_page_share_add_users": "Добавить пользователей",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Динамическое расположение",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Группировать объекты по",
"asset_list_layout_settings_group_by_month": "месяцу",
"asset_list_layout_settings_group_by_month_day": "месяцу и дню",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Добавить в альбом",
"control_bottom_app_bar_album_info": "{} файлов",
"control_bottom_app_bar_album_info_shared": "{} файлов · Общий",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "\nСоздать новый альбом",
"control_bottom_app_bar_delete": "Удалить",
"control_bottom_app_bar_favorite": "Избранное",
"control_bottom_app_bar_share": "Поделиться",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Без названия",
"create_shared_album_page_create": "Создать",
"create_shared_album_page_share": "Поделиться",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Отменить",
"delete_dialog_ok": "Удалить",
"delete_dialog_title": "Удалить навсегда",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Добавить описание...",
"exif_bottom_sheet_details": "ПОДРОБНОСТИ",
"exif_bottom_sheet_location": "МЕСТОПОЛОЖЕНИЕ",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий",
"experimental_settings_subtitle": "Используйте на свой страх и риск!",
"experimental_settings_title": "Экспериментальные функции",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Избранное",
"home_page_add_to_album_conflicts": "Добавлено {added} объектов в альбом {album}. Объекты {failed} уже есть в альбоме.",
"home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропускаем",
"home_page_add_to_album_success": "Добавлено {added} объектов в альбом {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Построение временной шкалы",
"home_page_favorite_err_local": "Пока не удается добавить в избранное локальные объекты, пропускаем",
"home_page_first_time_notice": "Если вы используете приложение впервые, убедитесь, что вы выбрали резервный(е) альбом(ы), чтобы временная шкала могла заполнить фотографии и видео в альбоме(ах).",
"image_viewer_page_state_provider_download_error": "Ошибка загрузки",
"image_viewer_page_state_provider_download_success": "Успешно загружено",
"library_page_albums": "Альбомы",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Избранное",
"library_page_new_album": "Новый альбом",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Pridané do {album}",
"add_to_album_bottom_sheet_already_exists": "Už v {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "VYLÚČENÉ",
"album_info_card_backup_album_included": "ZAHRNUTÉ",
"album_thumbnail_card_item": "1 položka",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Odstrániť z albumu",
"album_viewer_page_share_add_users": "Pridať používateľov",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamické rozloženie",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Zoskupiť položky podľa",
"asset_list_layout_settings_group_by_month": "Mesiac",
"asset_list_layout_settings_group_by_month_day": "Mesiac + deň",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Pridať do albumu",
"control_bottom_app_bar_album_info": "{} položiek",
"control_bottom_app_bar_album_info_shared": "{} položky - zdieľané",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Vytvoriť nový album",
"control_bottom_app_bar_delete": "Vymazať",
"control_bottom_app_bar_favorite": "Obľúbené",
"control_bottom_app_bar_share": "Zdieľať",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Bez názvu",
"create_shared_album_page_create": "Vytvoriť",
"create_shared_album_page_share": "Zdieľať",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Zrušiť",
"delete_dialog_ok": "Vymazať",
"delete_dialog_title": "Vymazať natrvalo",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Pridať popis...",
"exif_bottom_sheet_details": "PODROBNOSTI",
"exif_bottom_sheet_location": "LOKALITA",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Povolenie experimentálnej mriežky fotografií",
"experimental_settings_subtitle": "Používajte na vlastné riziko!",
"experimental_settings_title": "Experimentálne",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Obľúbené",
"home_page_add_to_album_conflicts": "Pridané {added} položiek do albumu {album}. {failed} položiek už je v albume.",
"home_page_add_to_album_err_local": "Zatiaľ nie je možné pridať lokálne média do albumov, preskakuje sa",
"home_page_add_to_album_success": "Pridané {added} položky do albumu {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Vytváranie časovej osi",
"home_page_favorite_err_local": "Zatiaľ nie je možné zaradiť lokálne média medzi obľúbené, preskakuje sa",
"home_page_first_time_notice": "Ak aplikáciu používate prvý krát, nezabudnite si vybrať zálohované albumy, aby sa na časovej osi mohli nachádzať fotografie a videá z vybraných albumoch.",
"image_viewer_page_state_provider_download_error": "Chyba sťahovania",
"image_viewer_page_state_provider_download_success": "Sťahovanie bolo úspešné",
"library_page_albums": "Albumy",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Obľúbené",
"library_page_new_album": "Nový album",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "EXKLUDERAD",
"album_info_card_backup_album_included": "INKLUDERAD",
"album_thumbnail_card_item": "1 objekt",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "Ta bort från album",
"album_viewer_page_share_add_users": "Lägg till användare",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Gruppera bilder efter",
"asset_list_layout_settings_group_by_month": "Månad",
"asset_list_layout_settings_group_by_month_day": "Månad + dag",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "Lägg till i album",
"control_bottom_app_bar_album_info": "{} objekt",
"control_bottom_app_bar_album_info_shared": "{} objekt • Delat",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Skapa nytt album",
"control_bottom_app_bar_delete": "Radera",
"control_bottom_app_bar_favorite": "Favorit",
"control_bottom_app_bar_share": "Dela",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "Namnlös",
"create_shared_album_page_create": "Skapa",
"create_shared_album_page_share": "Dela",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "Avbryt",
"delete_dialog_ok": "Radera",
"delete_dialog_title": "Radera permanent",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Lägg till beskrivning...",
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "PLATS",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "Aktivera experimentellt fotorutnät",
"experimental_settings_subtitle": "Använd på egen risk!",
"experimental_settings_title": "Experimentellt",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favoriter",
"home_page_add_to_album_conflicts": "Lade till {added} foton och videor i albumet {album}. {failed} foton och videor finns redan i albumet.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Lade till {added} foton och videor i albumet {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Bygger tidslinjen",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "Om det här är första gången du använder appen, välj ett eller flera backup-album så att tidslinjen kan fyllas med foton och videor från albumen.",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"library_page_albums": "Album",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favoriter",
"library_page_new_album": "Nytt album",

View File

@@ -1,6 +1,10 @@
{
"add_to_album_bottom_sheet_added": "添加到{album}",
"add_to_album_bottom_sheet_already_exists": "已经在{album}中了",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"album_info_card_backup_album_excluded": "排除",
"album_info_card_backup_album_included": "已选",
"album_thumbnail_card_item": "1张",
@@ -17,7 +21,10 @@
"album_viewer_appbar_share_remove": "从相册中移除",
"album_viewer_page_share_add_users": "新增用户",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "动态布局",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "分组照片或视频由",
"asset_list_layout_settings_group_by_month": "月",
"asset_list_layout_settings_group_by_month_day": "月和日",
@@ -108,10 +115,12 @@
"control_bottom_app_bar_add_to_album": "添加到相册",
"control_bottom_app_bar_album_info": "{}张",
"control_bottom_app_bar_album_info_shared": "{} 张已分享",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "新建相册",
"control_bottom_app_bar_delete": "删除",
"control_bottom_app_bar_favorite": "收藏",
"control_bottom_app_bar_share": "分享",
"control_bottom_app_bar_unarchive": "Unarchive",
"create_album_page_untitled": "未命名",
"create_shared_album_page_create": "新建",
"create_shared_album_page_share": "分享",
@@ -126,6 +135,8 @@
"delete_dialog_cancel": "取消",
"delete_dialog_ok": "删除",
"delete_dialog_title": "永久删除",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "增加描述...",
"exif_bottom_sheet_details": "详情",
"exif_bottom_sheet_location": "位置",
@@ -133,16 +144,19 @@
"experimental_settings_new_asset_list_title": "启用实验性的照片宫格",
"experimental_settings_subtitle": "使用风险自负!",
"experimental_settings_title": "实验功能",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "收藏",
"home_page_add_to_album_conflicts": "添加{added}张到相册{album}。{failed} 项已经处于该相册中。",
"home_page_add_to_album_err_local": "无法在相册中收藏本地的照片或视频,跳过",
"home_page_add_to_album_success": "添加了{added}张到相册{album}。",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "生成时间线",
"home_page_favorite_err_local": "还不能收藏本地的照片或视频,跳过",
"home_page_first_time_notice": "如果这是您第一次使用该应用程序,请确保选择一个想要备份的本地相册,以便可以在时间线中预览该相册中的照片和视频。",
"image_viewer_page_state_provider_download_error": "下载出现错误",
"image_viewer_page_state_provider_download_success": "下载成功",
"library_page_albums": "相册",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "收藏",
"library_page_new_album": "新建相册",

View File

@@ -35,6 +35,14 @@ target 'Runner' do
end
post_install do |installer|
installer.generated_projects.each do |project|
project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end
end
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)

View File

@@ -61,12 +61,12 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
@@ -100,7 +100,7 @@ EXTERNAL SOURCES:
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios"
:path: ".symlinks/plugins/path_provider_foundation/darwin"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
permission_handler_apple:
@@ -110,7 +110,7 @@ EXTERNAL SOURCES:
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
@@ -129,7 +129,7 @@ SPEC CHECKSUMS:
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: 58b9c4269cb176f89acea5e5d043c9358f2d25f8
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
integration_test: 13825b8a9334a850581300559b8839134b124670
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
@@ -145,6 +145,6 @@ SPEC CHECKSUMS:
video_player_avfoundation: 6d971a232d72e6ee25368378d48a079dea01f1cf
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: 0606648e8a9ecd5a59eafa5ab3187b45a9004a28
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.11.3

View File

@@ -220,6 +220,7 @@
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -378,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 94;
CURRENT_PROJECT_VERSION = 95;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -514,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 94;
CURRENT_PROJECT_VERSION = 95;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -542,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 94;
CURRENT_PROJECT_VERSION = 95;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -45,11 +45,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.54.0</string>
<string>1.55.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>94</string>
<string>95</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.55.0"
version_number: "1.56.2"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,34 +5,29 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000307">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000282">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="3.674618">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.815995">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="94.327489">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="30.927419">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.317998">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.464698">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="86.596447">
<testcase classname="fastlane.lanes" name="4: build_app" time="66.988561">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="8.496988">
<failure message="/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:30:in `block (2 levels) in parsing_binding&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `load&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Error uploading ipa file: &#10; [Application Loader Error Output]: Error uploading &apos;/var/folders/_5/z27flzxx02j89sxdq8f1rwqc0000gn/T/77c4b65b-64d8-4545-8f0b-74399639f049.ipa&apos;.
<failure message="/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:27:in `block (2 levels) in parsing_binding&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `load&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Error packaging up the application" />
</testcase>

View File

@@ -2,47 +2,20 @@ import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/asset.dart';
class AssetSelectionPageResult {
final Set<Asset> selectedNewAsset;
final Set<Asset> selectedAdditionalAsset;
final bool isAlbumExist;
final Set<Asset> selectedAssets;
AssetSelectionPageResult({
required this.selectedNewAsset,
required this.selectedAdditionalAsset,
required this.isAlbumExist,
required this.selectedAssets,
});
AssetSelectionPageResult copyWith({
Set<Asset>? selectedNewAsset,
Set<Asset>? selectedAdditionalAsset,
bool? isAlbumExist,
}) {
return AssetSelectionPageResult(
selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
selectedAdditionalAsset:
selectedAdditionalAsset ?? this.selectedAdditionalAsset,
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
);
}
@override
String toString() =>
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
final setEquals = const DeepCollectionEquality().equals;
return other is AssetSelectionPageResult &&
setEquals(other.selectedNewAsset, selectedNewAsset) &&
setEquals(other.selectedAdditionalAsset, selectedAdditionalAsset) &&
other.isAlbumExist == isAlbumExist;
setEquals(other.selectedAssets, selectedAssets);
}
@override
int get hashCode =>
selectedNewAsset.hashCode ^
selectedAdditionalAsset.hashCode ^
isAlbumExist.hashCode;
int get hashCode => selectedAssets.hashCode;
}

View File

@@ -1,77 +0,0 @@
import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/asset.dart';
class AssetSelectionState {
final Set<String> selectedMonths;
final Set<Asset> selectedNewAssetsForAlbum;
final Set<Asset> selectedAdditionalAssetsForAlbum;
final Set<Asset> selectedAssetsInAlbumViewer;
final bool isMultiselectEnable;
/// Indicate the asset selection page is navigated from existing album
final bool isAlbumExist;
AssetSelectionState({
required this.selectedMonths,
required this.selectedNewAssetsForAlbum,
required this.selectedAdditionalAssetsForAlbum,
required this.selectedAssetsInAlbumViewer,
required this.isMultiselectEnable,
required this.isAlbumExist,
});
AssetSelectionState copyWith({
Set<String>? selectedMonths,
Set<Asset>? selectedNewAssetsForAlbum,
Set<Asset>? selectedAdditionalAssetsForAlbum,
Set<Asset>? selectedAssetsInAlbumViewer,
bool? isMultiselectEnable,
bool? isAlbumExist,
}) {
return AssetSelectionState(
selectedMonths: selectedMonths ?? this.selectedMonths,
selectedNewAssetsForAlbum:
selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ??
this.selectedAdditionalAssetsForAlbum,
selectedAssetsInAlbumViewer:
selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
);
}
@override
String toString() {
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
final setEquals = const DeepCollectionEquality().equals;
return other is AssetSelectionState &&
setEquals(other.selectedMonths, selectedMonths) &&
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
setEquals(
other.selectedAdditionalAssetsForAlbum,
selectedAdditionalAssetsForAlbum,
) &&
setEquals(
other.selectedAssetsInAlbumViewer,
selectedAssetsInAlbumViewer,
) &&
other.isMultiselectEnable == isMultiselectEnable &&
other.isAlbumExist == isAlbumExist;
}
@override
int get hashCode {
return selectedMonths.hashCode ^
selectedNewAssetsForAlbum.hashCode ^
selectedAdditionalAssetsForAlbum.hashCode ^
selectedAssetsInAlbumViewer.hashCode ^
isMultiselectEnable.hashCode ^
isAlbumExist.hashCode;
}
}

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@@ -9,50 +10,38 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class AlbumNotifier extends StateNotifier<List<Album>> {
AlbumNotifier(this._albumService, this._db) : super([]);
AlbumNotifier(this._albumService, Isar db) : super([]) {
final query = db.albums
.filter()
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
query.findAll().then((value) => state = value);
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService _albumService;
final Isar _db;
late final StreamSubscription<List<Album>> _streamSub;
Future<void> getAllAlbums() async {
final User me = Store.get(StoreKey.currentUser);
List<Album> albums = await _db.albums
.filter()
.owner((q) => q.isarIdEqualTo(me.isarId))
.findAll();
if (!const ListEquality().equals(albums, state)) {
state = albums;
}
await Future.wait([
_albumService.refreshDeviceAlbums(),
_albumService.refreshRemoteAlbums(isShared: false),
]);
albums = await _db.albums
.filter()
.owner((q) => q.isarIdEqualTo(me.isarId))
.findAll();
if (!const ListEquality().equals(albums, state)) {
state = albums;
}
}
Future<void> getAllAlbums() => Future.wait([
_albumService.refreshDeviceAlbums(),
_albumService.refreshRemoteAlbums(isShared: false),
]);
Future<bool> deleteAlbum(Album album) async {
state = state.where((a) => a.id != album.id).toList();
return _albumService.deleteAlbum(album);
}
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
Future<Album?> createAlbum(
String albumTitle,
Set<Asset> assets,
) async {
Album? album = await _albumService.createAlbum(albumTitle, assets, []);
if (album != null) {
state = [...state, album];
}
return album;
) =>
_albumService.createAlbum(albumTitle, assets, []);
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final albumProvider = StateNotifierProvider<AlbumNotifier, List<Album>>((ref) {
final albumProvider =
StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
return AlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),

View File

@@ -1,134 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
import 'package:immich_mobile/shared/models/asset.dart';
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
AssetSelectionNotifier()
: super(
AssetSelectionState(
selectedNewAssetsForAlbum: {},
selectedMonths: {},
selectedAdditionalAssetsForAlbum: {},
selectedAssetsInAlbumViewer: {},
isAlbumExist: false,
isMultiselectEnable: false,
),
);
void setIsAlbumExist(bool isAlbumExist) {
state = state.copyWith(isAlbumExist: isAlbumExist);
}
void removeAssetsInMonth(
String removedMonth,
List<Asset> assetsInMonth,
) {
Set<Asset> currentAssetList = state.selectedNewAssetsForAlbum;
Set<String> currentMonthList = state.selectedMonths;
currentMonthList
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
for (Asset asset in assetsInMonth) {
currentAssetList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(
selectedNewAssetsForAlbum: currentAssetList,
selectedMonths: currentMonthList,
);
}
void addAdditionalAssets(List<Asset> assets) {
state = state.copyWith(
selectedAdditionalAssetsForAlbum: {
...state.selectedAdditionalAssetsForAlbum,
...assets
},
);
}
void addAllAssetsInMonth(String month, List<Asset> assetsInMonth) {
state = state.copyWith(
selectedMonths: {...state.selectedMonths, month},
selectedNewAssetsForAlbum: {
...state.selectedNewAssetsForAlbum,
...assetsInMonth
},
);
}
void addNewAssets(Iterable<Asset> assets) {
state = state.copyWith(
selectedNewAssetsForAlbum: {
...state.selectedNewAssetsForAlbum,
...assets
},
);
}
void removeSelectedNewAssets(List<Asset> assets) {
Set<Asset> currentList = state.selectedNewAssetsForAlbum;
for (Asset asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
}
void removeSelectedAdditionalAssets(List<Asset> assets) {
Set<Asset> currentList = state.selectedAdditionalAssetsForAlbum;
for (Asset asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(selectedAdditionalAssetsForAlbum: currentList);
}
void removeAll() {
state = state.copyWith(
selectedNewAssetsForAlbum: {},
selectedMonths: {},
selectedAdditionalAssetsForAlbum: {},
selectedAssetsInAlbumViewer: {},
isAlbumExist: false,
);
}
void enableMultiselection() {
state = state.copyWith(isMultiselectEnable: true);
}
void disableMultiselection() {
state = state.copyWith(
isMultiselectEnable: false,
selectedAssetsInAlbumViewer: {},
);
}
void addAssetsInAlbumViewer(List<Asset> assets) {
state = state.copyWith(
selectedAssetsInAlbumViewer: {
...state.selectedAssetsInAlbumViewer,
...assets
},
);
}
void removeAssetsInAlbumViewer(List<Asset> assets) {
Set<Asset> currentList = state.selectedAssetsInAlbumViewer;
for (Asset asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(selectedAssetsInAlbumViewer: currentList);
}
}
final assetSelectionProvider =
StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
return AssetSelectionNotifier();
});

View File

@@ -1,7 +1,9 @@
import 'package:collection/collection.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/user.dart';
@@ -9,10 +11,14 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
SharedAlbumNotifier(this._albumService, this._db) : super([]);
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
query.findAll().then((value) => state = value);
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService _albumService;
final Isar _db;
late final StreamSubscription<List<Album>> _streamSub;
Future<Album?> createSharedAlbum(
String albumName,
@@ -20,46 +26,21 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
Iterable<User> sharedUsers,
) async {
try {
final Album? newAlbum = await _albumService.createAlbum(
return await _albumService.createAlbum(
albumName,
assets,
sharedUsers,
);
if (newAlbum != null) {
state = [...state, newAlbum];
return newAlbum;
}
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
}
return null;
}
Future<void> getAllSharedAlbums() async {
var albums = await _db.albums
.filter()
.sharedEqualTo(true)
.sortByCreatedAtDesc()
.findAll();
if (!const ListEquality().equals(albums, state)) {
state = albums;
}
await _albumService.refreshRemoteAlbums(isShared: true);
albums = await _db.albums
.filter()
.sharedEqualTo(true)
.sortByCreatedAtDesc()
.findAll();
if (!const ListEquality().equals(albums, state)) {
state = albums;
}
}
Future<void> getAllSharedAlbums() =>
_albumService.refreshRemoteAlbums(isShared: true);
Future<bool> deleteAlbum(Album album) {
state = state.where((a) => a.id != album.id).toList();
return _albumService.deleteAlbum(album);
}
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
Future<bool> leaveAlbum(Album album) async {
var res = await _albumService.leaveAlbum(album);
@@ -75,10 +56,16 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
return _albumService.removeAssetFromAlbum(album, assets);
}
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<Album>>((ref) {
StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
return SharedAlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
@@ -86,10 +73,15 @@ final sharedAlbumProvider =
});
final sharedAlbumDetailProvider =
FutureProvider.autoDispose.family<Album?, int>((ref, albumId) async {
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
final Album? a = await sharedAlbumService.getAlbumDetail(albumId);
await a?.loadSortedAssets();
return a;
await for (final a in sharedAlbumService.watchAlbum(albumId)) {
if (a == null) {
throw Exception("Album with ID=$albumId does not exist anymore!");
}
await for (final _ in a.watchRenderList(GroupAssetsBy.none)) {
yield a;
}
}
});

View File

@@ -214,8 +214,9 @@ class AlbumService {
);
}
Future<Album?> getAlbumDetail(int albumId) {
return _db.albums.get(albumId);
Stream<Album?> watchAlbum(int albumId) async* {
yield await _db.albums.get(albumId);
yield* _db.albums.watchObject(albumId);
}
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
@@ -110,12 +109,6 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
TextStyle(color: Theme.of(context).primaryColor),
),
onPressed: () {
ref
.watch(assetSelectionProvider.notifier)
.removeAll();
ref
.watch(assetSelectionProvider.notifier)
.addNewAssets(assets);
AutoRouter.of(context).push(
CreateAlbumRoute(
isSharedAlbum: false,

View File

@@ -9,12 +9,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
final List<Album> albums;
final List<Album> sharedAlbums;
final void Function(Album) onAddToAlbum;
final bool enabled;
const AddToAlbumSliverList({
Key? key,
required this.onAddToAlbum,
required this.albums,
required this.sharedAlbums,
this.enabled = true,
}) : super(key: key);
@override
@@ -28,14 +30,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ExpansionTile(
title: Text('common_shared'.tr()),
title: Text('common_shared'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
leading: const Icon(Icons.group),
children: sharedAlbums
.map(
(album) => AlbumThumbnailListTile(
album: album,
onTap: () => onAddToAlbum(album),
onTap: enabled ? () => onAddToAlbum(album) : () {},
),
)
.toList(),
@@ -48,7 +50,7 @@ class AddToAlbumSliverList extends HookConsumerWidget {
final album = albums[offset];
return AlbumThumbnailListTile(
album: album,
onTap: () => onAddToAlbum(album),
onTap: enabled ? () => onAddToAlbum(album) : () {},
);
}),
);

View File

@@ -5,29 +5,32 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
class AlbumViewerAppbar extends HookConsumerWidget
implements PreferredSizeWidget {
const AlbumViewerAppbar({
Key? key,
required this.album,
required this.userId,
required this.selected,
required this.selectionDisabled,
required this.titleFocusNode,
}) : super(key: key);
final Album album;
final String userId;
final Set<Asset> selected;
final void Function() selectionDisabled;
final FocusNode titleFocusNode;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isMultiSelectionEnable =
ref.watch(assetSelectionProvider).isMultiselectEnable;
final selectedAssetsInAlbum =
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
@@ -85,12 +88,12 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
bool isSuccess =
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
album,
selectedAssetsInAlbum,
selected,
);
if (isSuccess) {
Navigator.pop(context);
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
selectionDisabled();
ref.watch(albumProvider.notifier).getAllAlbums();
ref.invalidate(sharedAlbumDetailProvider(album.id));
} else {
@@ -107,7 +110,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
}
buildBottomSheetActionButton() {
if (isMultiSelectionEnable) {
if (selected.isNotEmpty) {
if (album.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_sweep_rounded),
@@ -162,11 +165,9 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
}
buildLeadingButton() {
if (isMultiSelectionEnable) {
if (selected.isNotEmpty) {
return IconButton(
onPressed: () => ref
.watch(assetSelectionProvider.notifier)
.disableMultiselection(),
onPressed: selectionDisabled,
icon: const Icon(Icons.close_rounded),
splashRadius: 25,
);
@@ -201,9 +202,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
return AppBar(
elevation: 0,
leading: buildLeadingButton(),
title: isMultiSelectionEnable
? Text('${selectedAssetsInAlbum.length}')
: null,
title: selected.isNotEmpty ? Text('${selected.length}') : null,
centerTitle: false,
actions: [
if (album.isRemote)

View File

@@ -1,163 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/utils/storage_indicator.dart';
class AlbumViewerThumbnail extends HookConsumerWidget {
final Asset asset;
final List<Asset> assetList;
final bool showStorageIndicator;
const AlbumViewerThumbnail({
Key? key,
required this.asset,
required this.assetList,
this.showStorageIndicator = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedAssetsInAlbumViewer =
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
final isMultiSelectionEnable =
ref.watch(assetSelectionProvider).isMultiselectEnable;
final isFavorite = ref.watch(favoriteProvider).contains(asset.id);
viewAsset() {
AutoRouter.of(context).push(
GalleryViewerRoute(
asset: asset,
assetList: assetList,
),
);
}
BoxBorder drawBorderColor() {
if (selectedAssetsInAlbumViewer.contains(asset)) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
);
} else {
return const Border();
}
}
enableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).enableMultiselection();
ref
.watch(assetSelectionProvider.notifier)
.addAssetsInAlbumViewer([asset]);
}
disableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
}
buildVideoLabel() {
return Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
);
}
buildAssetStoreLocationIcon() {
return Positioned(
right: 10,
bottom: 5,
child: Icon(
storageIcon(asset),
color: Colors.white,
size: 18,
),
);
}
buildAssetFavoriteIcon() {
return const Positioned(
left: 10,
bottom: 5,
child: Icon(
Icons.favorite,
color: Colors.white,
size: 18,
),
);
}
buildAssetSelectionIcon() {
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
return Positioned(
left: 10,
top: 5,
child: isSelected
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.white,
),
);
}
buildThumbnailImage() {
return Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: ImmichImage(asset, width: 300, height: 300),
);
}
handleSelectionGesture() {
if (selectedAssetsInAlbumViewer.contains(asset)) {
ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInAlbumViewer([asset]);
if (selectedAssetsInAlbumViewer.isEmpty) {
disableMultiSelection();
}
} else {
ref
.watch(assetSelectionProvider.notifier)
.addAssetsInAlbumViewer([asset]);
}
}
return GestureDetector(
onTap: isMultiSelectionEnable ? handleSelectionGesture : viewAsset,
onLongPress: enableMultiSelection,
child: Stack(
children: [
buildThumbnailImage(),
if (isFavorite) buildAssetFavoriteIcon(),
if (showStorageIndicator) buildAssetStoreLocationIcon(),
if (!asset.isImage) buildVideoLabel(),
if (isMultiSelectionEnable) buildAssetSelectionIcon(),
],
),
);
}
}

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
import 'package:immich_mobile/shared/models/asset.dart';
class AssetGridByMonth extends HookConsumerWidget {
final List<Asset> assetGroup;
const AssetGridByMonth({Key? key, required this.assetGroup})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return SelectionThumbnailImage(asset: assetGroup[index]);
},
childCount: assetGroup.length,
),
);
}
}

View File

@@ -1,117 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
class MonthGroupTitle extends HookConsumerWidget {
final String month;
final List<Asset> assetGroup;
const MonthGroupTitle({
Key? key,
required this.month,
required this.assetGroup,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
handleTitleIconClick() {
HapticFeedback.heavyImpact();
if (isAlbumExist) {
if (selectedDateGroup.contains(month)) {
ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInMonth(month, []);
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedAdditionalAssets(assetGroup);
} else {
ref
.watch(assetSelectionProvider.notifier)
.addAllAssetsInMonth(month, []);
// Deep clone assetGroup
var assetGroupWithNewItems = [...assetGroup];
for (var selectedAsset in selectedAssets) {
assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
}
ref
.watch(assetSelectionProvider.notifier)
.addAdditionalAssets(assetGroupWithNewItems);
}
} else {
if (selectedDateGroup.contains(month)) {
ref
.watch(assetSelectionProvider.notifier)
.removeAssetsInMonth(month, assetGroup);
} else {
ref
.watch(assetSelectionProvider.notifier)
.addAllAssetsInMonth(month, assetGroup);
}
}
}
getSimplifiedMonth() {
var monthAndYear = month.split(',');
var yearText = monthAndYear[1].trim();
var monthText = monthAndYear[0].trim();
var currentYear = DateTime.now().year.toString();
if (yearText == currentYear) {
return monthText;
} else {
return month;
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
top: 29.0,
bottom: 29.0,
left: 14.0,
right: 8.0,
),
child: Row(
children: [
GestureDetector(
onTap: handleTitleIconClick,
child: selectedDateGroup.contains(month)
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.circle_outlined,
color: Colors.grey,
),
),
GestureDetector(
onTap: handleTitleIconClick,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
getSimplifiedMonth(),
style: TextStyle(
fontSize: 24,
color: Theme.of(context).primaryColor,
),
),
),
),
],
),
),
);
}
}

View File

@@ -1,141 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
class SelectionThumbnailImage extends HookConsumerWidget {
final Asset asset;
const SelectionThumbnailImage({Key? key, required this.asset})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var selectedAsset =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
Widget buildSelectionIcon(Asset asset) {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isSelected && !isAlbumExist) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
} else if (isSelected && isAlbumExist) {
return const Icon(
Icons.check_circle,
color: Color.fromARGB(255, 233, 233, 233),
);
} else if (isNewlySelected && isAlbumExist) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
} else {
return const Icon(
Icons.circle_outlined,
color: Colors.white,
);
}
}
BoxBorder drawBorderColor() {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isSelected && !isAlbumExist) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
);
} else if (isSelected && isAlbumExist) {
return Border.all(
color: const Color.fromARGB(255, 190, 190, 190),
width: 10,
);
} else if (isNewlySelected && isAlbumExist) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
);
}
return const Border();
}
return GestureDetector(
onTap: () {
var isSelected =
selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isAlbumExist) {
// Operation for existing album
if (!isSelected) {
if (isNewlySelected) {
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedAdditionalAssets([asset]);
} else {
ref
.watch(assetSelectionProvider.notifier)
.addAdditionalAssets([asset]);
}
}
} else {
// Operation for new album
if (isSelected) {
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedNewAssets([asset]);
} else {
ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
}
}
},
child: Stack(
children: [
Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: ImmichImage(asset, width: 150, height: 150),
),
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: buildSelectionIcon(asset),
),
),
if (!asset.isImage)
Positioned(
bottom: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
),
],
),
);
}
}

View File

@@ -5,21 +5,18 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -32,33 +29,51 @@ class AlbumViewerPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
FocusNode titleFocusNode = useFocusNode();
ScrollController scrollController = useScrollController();
final album = ref.watch(sharedAlbumDetailProvider(albumId));
final userId = ref.watch(authenticationProvider).userId;
final selection = useState<Set<Asset>>({});
final multiSelectEnabled = useState(false);
bool? isTop;
Future<bool> onWillPop() async {
if (multiSelectEnabled.value) {
selection.value = {};
multiSelectEnabled.value = false;
return false;
}
return true;
}
void selectionListener(bool active, Set<Asset> selected) {
selection.value = selected;
multiSelectEnabled.value = selected.isNotEmpty;
}
void disableSelection() {
selection.value = {};
multiSelectEnabled.value = false;
}
/// Find out if the assets in album exist on the device
/// If they exist, add to selected asset state to show they are already selected.
void onAddPhotosPressed(Album albumInfo) async {
if (albumInfo.assets.isNotEmpty == true) {
ref.watch(assetSelectionProvider.notifier).addNewAssets(
albumInfo.assets,
);
}
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
AssetSelectionPageResult? returnPayload = await AutoRouter.of(context)
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
AssetSelectionPageResult? returnPayload =
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
AssetSelectionRoute(
existingAssets: albumInfo.assets,
isNewAlbum: false,
),
);
if (returnPayload != null) {
// Check if there is new assets add
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
if (returnPayload.selectedAssets.isNotEmpty) {
ImmichLoadingOverlayController.appLoader.show();
var addAssetsResult =
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
returnPayload.selectedAdditionalAsset,
returnPayload.selectedAssets,
albumInfo,
);
@@ -70,10 +85,6 @@ class AlbumViewerPage extends HookConsumerWidget {
ImmichLoadingOverlayController.appLoader.hide();
}
ref.watch(assetSelectionProvider.notifier).removeAll();
} else {
ref.watch(assetSelectionProvider.notifier).removeAll();
}
}
@@ -91,13 +102,38 @@ class AlbumViewerPage extends HookConsumerWidget {
.addAdditionalUserToAlbum(sharedUserIds, album);
if (isSuccess) {
ref.invalidate(sharedAlbumDetailProvider(albumId));
ref.invalidate(sharedAlbumDetailProvider(album.id));
}
ImmichLoadingOverlayController.appLoader.hide();
}
}
Widget buildControlButton(Album album) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
child: SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
AlbumActionOutlinedButton(
iconData: Icons.add_photo_alternate_outlined,
onPressed: () => onAddPhotosPressed(album),
labelText: "share_add_photos".tr(),
),
if (userId == album.ownerId)
AlbumActionOutlinedButton(
iconData: Icons.person_add_alt_rounded,
onPressed: () => onAddUsersPressed(album),
labelText: "album_viewer_page_share_add_users".tr(),
),
],
),
),
);
}
Widget buildTitle(Album album) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
@@ -146,171 +182,104 @@ class AlbumViewerPage extends HookConsumerWidget {
}
Widget buildHeader(Album album) {
return SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildTitle(album),
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
if (album.shared)
SizedBox(
height: 60,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: CircleAvatar(
backgroundColor: Colors.grey[300],
radius: 18,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(50.0),
child: Image.asset(
'assets/immich-logo-no-outline.png',
),
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildTitle(album),
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
if (album.shared)
SizedBox(
height: 50,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: CircleAvatar(
backgroundColor: Colors.grey[300],
radius: 18,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(50.0),
child: Image.asset(
'assets/immich-logo-no-outline.png',
),
),
),
);
}),
itemCount: album.sharedUsers.length,
),
)
],
),
);
}
Widget buildImageGrid(Album album) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final bool showStorageIndicator =
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
if (album.sortedAssets.isNotEmpty) {
return SliverPadding(
padding: const EdgeInsets.only(top: 10.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return AlbumViewerThumbnail(
asset: album.sortedAssets[index],
assetList: album.sortedAssets,
showStorageIndicator: showStorageIndicator,
);
},
childCount: album.assetCount,
),
),
);
}
return const SliverToBoxAdapter();
}
Widget buildControlButton(Album album) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
child: SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
AlbumActionOutlinedButton(
iconData: Icons.add_photo_alternate_outlined,
onPressed: () => onAddPhotosPressed(album),
labelText: "share_add_photos".tr(),
),
if (userId == album.ownerId)
AlbumActionOutlinedButton(
iconData: Icons.person_add_alt_rounded,
onPressed: () => onAddUsersPressed(album),
labelText: "album_viewer_page_share_add_users".tr(),
),
],
),
),
);
}
Future<bool> onWillPop() async {
final isMultiselectEnable = ref
.read(assetSelectionProvider)
.selectedAssetsInAlbumViewer
.isNotEmpty;
if (isMultiselectEnable) {
ref.watch(assetSelectionProvider.notifier).removeAll();
return false;
}
return true;
}
Widget buildBody(Album album) {
return WillPopScope(
onWillPop: onWillPop,
child: GestureDetector(
onTap: () {
titleFocusNode.unfocus();
},
child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
heightScrollThumb: 48.0,
child: CustomScrollView(
controller: scrollController,
slivers: [
buildHeader(album),
if (album.isRemote)
SliverPersistentHeader(
pinned: true,
delegate: ImmichSliverPersistentAppBarDelegate(
minHeight: 50,
maxHeight: 50,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: buildControlButton(album),
),
),
),
SliverSafeArea(
sliver: buildImageGrid(album),
),
],
);
}),
itemCount: album.sharedUsers.length,
),
),
),
),
],
);
}
final scroll = ScrollController();
return Scaffold(
appBar: album.when(
data: (Album? data) {
if (data != null) {
return AlbumViewerAppbar(
album: data,
userId: userId,
);
}
return null;
},
error: (e, _) => null,
loading: () => null,
data: (data) => AlbumViewerAppbar(
titleFocusNode: titleFocusNode,
album: data,
userId: userId,
selected: selection.value,
selectionDisabled: disableSelection,
),
error: (error, stackTrace) => AppBar(title: const Text("Error")),
loading: () => AppBar(),
),
body: album.when(
data: (albumInfo) => albumInfo != null
? buildBody(albumInfo)
: const Center(
child: CircularProgressIndicator(),
data: (data) => WillPopScope(
onWillPop: onWillPop,
child: GestureDetector(
onTap: () {
titleFocusNode.unfocus();
},
child: NestedScrollView(
controller: scroll,
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverToBoxAdapter(child: buildHeader(data)),
SliverPersistentHeader(
pinned: true,
delegate: ImmichSliverPersistentAppBarDelegate(
minHeight: 50,
maxHeight: 50,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: buildControlButton(data),
),
),
)
],
body: ImmichAssetGrid(
renderList: data.renderList,
listener: selectionListener,
selectionActive: multiSelectEnabled.value,
showMultiSelectIndicator: false,
visibleItemsListener: (start, end) {
final top = start.index == 0 && start.itemLeadingEdge == 0.0;
if (top != isTop) {
isTop = top;
scroll.animateTo(
top
? scroll.position.minScrollExtent
: scroll.position.maxScrollExtent,
duration: const Duration(milliseconds: 500),
curve: top ? Curves.easeOut : Curves.easeIn,
);
}
},
),
error: (e, _) => Center(child: Text("Error loading album info $e")),
),
),
),
error: (e, _) => Center(child: Text("Error loading album info!\n$e")),
loading: () => const Center(
child: ImmichLoadingIndicator(),
),

View File

@@ -4,54 +4,42 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/ui/asset_grid_by_month.dart';
import 'package:immich_mobile/modules/album/ui/month_group_title.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
class AssetSelectionPage extends HookConsumerWidget {
const AssetSelectionPage({Key? key}) : super(key: key);
const AssetSelectionPage({
Key? key,
required this.existingAssets,
this.isNewAlbum = false,
}) : super(key: key);
final Set<Asset> existingAssets;
final bool isNewAlbum;
@override
Widget build(BuildContext context, WidgetRef ref) {
ScrollController scrollController = useScrollController();
var assetGroupMonthYear = ref.watch(assetGroupByMonthYearProvider);
final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
List<Widget> imageGridGroup = [];
final renderList = ref.watch(remoteAssetsProvider);
final selected = useState<Set<Asset>>(existingAssets);
final selectionEnabledHook = useState(true);
String buildAssetCountText() {
if (isAlbumExist) {
return (selectedAssets.length + newAssetsForAlbum.length).toString();
} else {
return selectedAssets.length.toString();
}
return selected.value.length.toString();
}
Widget buildBody() {
assetGroupMonthYear.forEach((monthYear, assetGroup) {
imageGridGroup
.add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
imageGridGroup.add(AssetGridByMonth(assetGroup: assetGroup));
});
return Stack(
children: [
DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
heightScrollThumb: 48.0,
child: CustomScrollView(
controller: scrollController,
slivers: [...imageGridGroup],
),
),
],
Widget buildBody(RenderList renderList) {
return ImmichAssetGrid(
renderList: renderList,
listener: (active, assets) {
selectionEnabledHook.value = active;
selected.value = assets;
},
selectionActive: true,
preselectedAssets: isNewAlbum ? selected.value : existingAssets,
canDeselect: isNewAlbum,
showMultiSelectIndicator: false,
);
}
@@ -61,11 +49,10 @@ class AssetSelectionPage extends HookConsumerWidget {
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
ref.watch(assetSelectionProvider.notifier).removeAll();
AutoRouter.of(context).pop(null);
AutoRouter.of(context).popForced(null);
},
),
title: selectedAssets.isEmpty
title: selected.value.isEmpty
? const Text(
'share_add_photos',
style: TextStyle(fontSize: 18),
@@ -76,16 +63,13 @@ class AssetSelectionPage extends HookConsumerWidget {
),
centerTitle: false,
actions: [
if ((!isAlbumExist && selectedAssets.isNotEmpty) ||
(isAlbumExist && newAssetsForAlbum.isNotEmpty))
if (selected.value.isNotEmpty)
TextButton(
onPressed: () {
var payload = AssetSelectionPageResult(
isAlbumExist: isAlbumExist,
selectedAdditionalAsset: newAssetsForAlbum,
selectedNewAsset: selectedAssets,
);
AutoRouter.of(context).pop(payload);
var payload =
AssetSelectionPageResult(selectedAssets: selected.value);
AutoRouter.of(context)
.popForced<AssetSelectionPageResult>(payload);
},
child: const Text(
"share_add",
@@ -94,7 +78,13 @@ class AssetSelectionPage extends HookConsumerWidget {
),
],
),
body: buildBody(),
body: renderList.when(
data: (data) => buildBody(data),
error: (error, stackTrace) => Center(
child: Text(error.toString()),
),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
}
}

View File

@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
@@ -31,12 +30,15 @@ class CreateAlbumPage extends HookConsumerWidget {
final albumTitleTextFieldFocusNode = useFocusNode();
final isAlbumTitleTextFieldFocus = useState(false);
final isAlbumTitleEmpty = useState(true);
final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final selectedAssets = useState<Set<Asset>>(const {});
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
showSelectUserPage() {
AutoRouter.of(context).push(const SelectUserForSharingRoute());
showSelectUserPage() async {
final bool? ok = await AutoRouter.of(context)
.push<bool?>(SelectUserForSharingRoute(assets: selectedAssets.value));
if (ok == true) {
selectedAssets.value = {};
}
}
void onBackgroundTapped() {
@@ -52,13 +54,17 @@ class CreateAlbumPage extends HookConsumerWidget {
}
onSelectPhotosButtonPressed() async {
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
AssetSelectionPageResult? selectedAsset =
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
AssetSelectionRoute(
existingAssets: selectedAssets.value,
isNewAlbum: true,
),
);
if (selectedAsset == null) {
ref.watch(assetSelectionProvider.notifier).removeAll();
selectedAssets.value = const {};
} else {
selectedAssets.value = selectedAsset.selectedAssets;
}
}
@@ -78,7 +84,7 @@ class CreateAlbumPage extends HookConsumerWidget {
}
buildTitle() {
if (selectedAssets.isEmpty) {
if (selectedAssets.value.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 200, left: 18),
@@ -97,7 +103,7 @@ class CreateAlbumPage extends HookConsumerWidget {
}
buildSelectPhotosButton() {
if (selectedAssets.isEmpty) {
if (selectedAssets.value.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
@@ -158,7 +164,7 @@ class CreateAlbumPage extends HookConsumerWidget {
}
buildSelectedImageGrid() {
if (selectedAssets.isNotEmpty) {
if (selectedAssets.value.isNotEmpty) {
return SliverPadding(
padding: const EdgeInsets.only(top: 16),
sliver: SliverGrid(
@@ -172,11 +178,11 @@ class CreateAlbumPage extends HookConsumerWidget {
return GestureDetector(
onTap: onBackgroundTapped,
child: SharedAlbumThumbnailImage(
asset: selectedAssets.elementAt(index),
asset: selectedAssets.value.elementAt(index),
),
);
},
childCount: selectedAssets.length,
childCount: selectedAssets.value.length,
),
),
);
@@ -188,12 +194,12 @@ class CreateAlbumPage extends HookConsumerWidget {
createNonSharedAlbum() async {
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
selectedAssets.value,
);
if (newAlbum != null) {
ref.watch(albumProvider.notifier).getAllAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll();
selectedAssets.value = {};
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
@@ -207,7 +213,7 @@ class CreateAlbumPage extends HookConsumerWidget {
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton(
onPressed: () {
ref.watch(assetSelectionProvider.notifier).removeAll();
selectedAssets.value = {};
AutoRouter.of(context).pop();
},
icon: const Icon(Icons.close_rounded),
@@ -237,7 +243,7 @@ class CreateAlbumPage extends HookConsumerWidget {
if (!isSharedAlbum)
TextButton(
onPressed: albumTitleController.text.isNotEmpty &&
selectedAssets.isNotEmpty
selectedAssets.value.isNotEmpty
? createNonSharedAlbum
: null,
child: Text(
@@ -264,7 +270,7 @@ class CreateAlbumPage extends HookConsumerWidget {
child: Column(
children: [
buildTitleInputField(),
if (selectedAssets.isNotEmpty) buildControlButton(),
if (selectedAssets.value.isNotEmpty) buildControlButton(),
],
),
),

View File

@@ -4,15 +4,18 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class SelectUserForSharingPage extends HookConsumerWidget {
const SelectUserForSharingPage({Key? key}) : super(key: key);
const SelectUserForSharingPage({Key? key, required this.assets})
: super(key: key);
final Set<Asset> assets;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -24,15 +27,15 @@ class SelectUserForSharingPage extends HookConsumerWidget {
var newAlbum =
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
assets,
sharedUsersList.value,
);
if (newAlbum != null) {
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll();
// ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
AutoRouter.of(context).pop(true);
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
}

View File

@@ -85,15 +85,15 @@ class SharingPage extends HookConsumerWidget {
),
),
subtitle: isOwner
? const Text(
'Owned',
style: TextStyle(
? Text(
'album_thumbnail_owned'.tr(),
style: const TextStyle(
fontSize: 12.0,
),
)
: album.ownerName != null
? Text(
'Shared by ${album.ownerName!}',
'album_thumbnail_shared_by'.tr(args: [album.ownerName!]),
style: const TextStyle(
fontSize: 12.0,
),

View File

@@ -1,55 +1,25 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class ArchiveSelectionNotifier extends StateNotifier<Set<int>> {
ArchiveSelectionNotifier(this.db, this.assetNotifier) : super({}) {
state = db.assets
.filter()
.isArchivedEqualTo(true)
.findAllSync()
.map((e) => e.id)
.toSet();
final archiveProvider = StreamProvider<RenderList>((ref) async* {
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.isArchivedEqualTo(true)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
yield await RenderList.fromQuery(query, groupBy);
await for (final _ in query.watchLazy()) {
yield await RenderList.fromQuery(query, groupBy);
}
final Isar db;
final AssetNotifier assetNotifier;
void _setArchiveForAssetId(int id, bool archive) {
if (!archive) {
state = state.difference({id});
} else {
state = state.union({id});
}
}
bool _isArchive(int id) {
return state.contains(id);
}
Future<void> toggleArchive(Asset asset) async {
if (asset.storage == AssetState.local) return;
_setArchiveForAssetId(asset.id, !_isArchive(asset.id));
await assetNotifier.toggleArchive(
[asset],
state.contains(asset.id),
);
}
Future<void> addToArchives(Iterable<Asset> assets) {
state = state.union(assets.map((a) => a.id).toSet());
return assetNotifier.toggleArchive(assets, true);
}
}
final archiveProvider =
StateNotifierProvider<ArchiveSelectionNotifier, Set<int>>((ref) {
return ArchiveSelectionNotifier(
ref.watch(dbProvider),
ref.watch(assetProvider.notifier),
);
});

View File

@@ -1,46 +1,25 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:isar/isar.dart';
class ArchivePage extends HookConsumerWidget {
const ArchivePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final User me = Store.get(StoreKey.currentUser);
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(me.isarId)
.isArchivedEqualTo(true);
final stream = query.watch();
final archivedAssets = useState<List<Asset>>([]);
final archivedAssets = ref.watch(archiveProvider);
final selectionEnabledHook = useState(false);
final selection = useState(<Asset>{});
useEffect(
() {
query.findAll().then((value) => archivedAssets.value = value);
final subscription = stream.listen((e) {
archivedAssets.value = e;
});
// Cancel the subscription when the widget is disposed
return subscription.cancel;
},
[],
);
final processing = useState(false);
void selectionListener(
bool multiselect,
@@ -50,7 +29,7 @@ class ArchivePage extends HookConsumerWidget {
selection.value = selectedAssets;
}
AppBar buildAppBar() {
AppBar buildAppBar(String count) {
return AppBar(
leading: IconButton(
onPressed: () => AutoRouter.of(context).pop(),
@@ -60,7 +39,7 @@ class ArchivePage extends HookConsumerWidget {
automaticallyImplyLeading: false,
title: const Text(
'archive_page_title',
).tr(args: [archivedAssets.value.length.toString()]),
).tr(args: [count]),
);
}
@@ -80,26 +59,38 @@ class ArchivePage extends HookConsumerWidget {
leading: const Icon(
Icons.unarchive_rounded,
),
title:
const Text("Unarchive", style: TextStyle(fontSize: 14)),
onTap: () {
if (selection.value.isNotEmpty) {
ref
.watch(assetProvider.notifier)
.toggleArchive(selection.value, false);
title: Text(
'control_bottom_app_bar_unarchive'.tr(),
style: const TextStyle(fontSize: 14),
),
onTap: processing.value
? null
: () async {
processing.value = true;
try {
if (selection.value.isNotEmpty) {
await ref
.watch(assetProvider.notifier)
.toggleArchive(
selection.value.toList(),
false,
);
final assetOrAssets =
selection.value.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg:
'Moved ${selection.value.length} $assetOrAssets to library',
gravity: ToastGravity.CENTER,
);
}
selectionEnabledHook.value = false;
},
final assetOrAssets = selection.value.length > 1
? 'assets'
: 'asset';
ImmichToast.show(
context: context,
msg:
'Moved ${selection.value.length} $assetOrAssets to library',
gravity: ToastGravity.CENTER,
);
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
},
)
],
),
@@ -109,22 +100,34 @@ class ArchivePage extends HookConsumerWidget {
);
}
return Scaffold(
appBar: buildAppBar(),
body: archivedAssets.value.isEmpty
? const Center(
child: Text('No archived assets found.'),
)
: Stack(
children: [
ImmichAssetGrid(
assets: archivedAssets.value,
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
),
if (selectionEnabledHook.value) buildBottomBar()
],
),
return archivedAssets.when(
loading: () => Scaffold(
appBar: buildAppBar("?"),
body: const Center(child: CircularProgressIndicator()),
),
error: (error, stackTrace) => Scaffold(
appBar: buildAppBar("Error"),
body: Center(child: Text(error.toString())),
),
data: (data) => Scaffold(
appBar: buildAppBar(data.totalAssets.toString()),
body: data.isEmpty
? Center(
child: Text('archive_page_no_archived_assets'.tr()),
)
: Stack(
children: [
ImmichAssetGrid(
renderList: data,
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
),
if (selectionEnabledHook.value) buildBottomBar(),
if (processing.value)
const Center(child: ImmichLoadingIndicator())
],
),
),
);
}
}

View File

@@ -1,6 +0,0 @@
class RequestDownloadAssetInfo {
final String assetId;
final String deviceId;
RequestDownloadAssetInfo(this.assetId, this.deviceId);
}

View File

@@ -4,14 +4,12 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
final renderListProvider = FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
var settings = ref.watch(appSettingsServiceProvider);
final renderListProvider =
FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
final settings = ref.watch(appSettingsServiceProvider);
final layout = AssetGridLayoutParameters(
settings.getSetting(AppSettingsEnum.tilesPerRow),
settings.getSetting(AppSettingsEnum.dynamicLayout),
return RenderList.fromAssets(
assets,
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
);
return RenderList.fromAssets(assets, layout);
});

View File

@@ -7,6 +7,7 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:latlong2/latlong.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:url_launcher/url_launcher.dart';
class ExifBottomSheet extends HookConsumerWidget {
final Asset asset;
@@ -42,19 +43,25 @@ class ExifBottomSheet extends HookConsumerWidget {
),
zoom: 16.0,
),
layers: [
TileLayerOptions(
nonRotatedChildren: [
RichAttributionWidget(
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () => launchUrl(
Uri.parse('https://openstreetmap.org/copyright'),
),
),
],
),
],
children: [
TileLayer(
urlTemplate:
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c'],
attributionBuilder: (_) {
return const Text(
"© OpenStreetMap",
style: TextStyle(fontSize: 10),
);
},
subdomains: const ['a', 'b', 'c'],
),
MarkerLayerOptions(
MarkerLayer(
markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
@@ -88,9 +95,9 @@ class ExifBottomSheet extends HookConsumerWidget {
}
buildDragHeader() {
return Column(
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
children: [
SizedBox(height: 12),
Align(
alignment: Alignment.center,

View File

@@ -21,7 +21,7 @@ class TopControlAppBar extends HookConsumerWidget {
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final VoidCallback onAddToAlbumPressed;
final VoidCallback onFavorite;
final VoidCallback? onFavorite;
final bool isPlayingMotionVideo;
final bool isFavorite;
@@ -31,9 +31,7 @@ class TopControlAppBar extends HookConsumerWidget {
Widget buildFavoriteButton() {
return IconButton(
onPressed: () {
onFavorite();
},
onPressed: onFavorite,
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: Colors.grey[200],

View File

@@ -1,5 +1,5 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
@@ -12,9 +12,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/services/asset.service.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
@@ -32,16 +30,16 @@ import 'package:openapi/api.dart' as api;
// ignore: must_be_immutable
class GalleryViewerPage extends HookConsumerWidget {
final List<Asset> assetList;
final Asset asset;
final Asset Function(int index) loadAsset;
final int totalAssets;
final int initialIndex;
GalleryViewerPage({
super.key,
required this.assetList,
required this.asset,
}) : controller = PageController(initialPage: assetList.indexOf(asset));
Asset? assetDetail;
required this.initialIndex,
required this.loadAsset,
required this.totalAssets,
}) : controller = PageController(initialPage: initialIndex);
final PageController controller;
@@ -52,11 +50,15 @@ class GalleryViewerPage extends HookConsumerWidget {
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
final isZoomed = useState<bool>(false);
final showAppBar = useState<bool>(true);
final indexOfAsset = useState(assetList.indexOf(asset));
final isPlayingMotionVideo = useState(false);
final isPlayingVideo = useState(false);
late Offset localPosition;
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
final currentIndex = useState(initialIndex);
final currentAsset = loadAsset(currentIndex.value);
final watchedAsset = ref.watch(assetDetailProvider(currentAsset));
Asset asset() => watchedAsset.value ?? currentAsset;
showAppBar.addListener(() {
// Change to and from immersive mode, hiding navigation and app bar
@@ -79,16 +81,9 @@ class GalleryViewerPage extends HookConsumerWidget {
[],
);
void toggleFavorite(Asset asset) {
ref.watch(favoriteProvider.notifier).toggleFavorite(asset);
}
void getAssetExif() async {
assetDetail = assetList[indexOfAsset.value];
assetDetail = await ref
.watch(assetServiceProvider)
.loadExif(assetList[indexOfAsset.value]);
}
void toggleFavorite(Asset asset) => ref
.watch(assetProvider.notifier)
.toggleFavorite([asset], !asset.isFavorite);
/// Thumbnail image of a remote asset. Required asset.isRemote
ImageProvider remoteThumbnailImageProvider(
@@ -138,8 +133,8 @@ class GalleryViewerPage extends HookConsumerWidget {
}
void precacheNextImage(int index) {
if (index < assetList.length && index >= 0) {
final asset = assetList[index];
if (index < totalAssets && index >= 0) {
final asset = loadAsset(index);
if (asset.isLocal) {
// Preload the local asset
@@ -193,13 +188,13 @@ class GalleryViewerPage extends HookConsumerWidget {
if (ref
.watch(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) {
return AdvancedBottomSheet(assetDetail: assetDetail!);
return AdvancedBottomSheet(assetDetail: asset());
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: ExifBottomSheet(asset: assetDetail!),
child: ExifBottomSheet(asset: asset()),
);
},
);
@@ -211,7 +206,7 @@ class GalleryViewerPage extends HookConsumerWidget {
builder: (BuildContext _) {
return DeleteDialog(
onDelete: () {
if (assetList.length == 1) {
if (totalAssets == 1) {
// Handle only one asset
AutoRouter.of(context).pop();
} else {
@@ -221,7 +216,6 @@ class GalleryViewerPage extends HookConsumerWidget {
curve: Curves.fastLinearToSlowEaseIn,
);
}
assetList.remove(deleteAsset);
ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
},
);
@@ -267,9 +261,7 @@ class GalleryViewerPage extends HookConsumerWidget {
}
shareAsset() {
ref
.watch(imageViewerStateProvider.notifier)
.shareAsset(assetList[indexOfAsset.value], context);
ref.watch(imageViewerStateProvider.notifier).shareAsset(asset(), context);
}
handleArchive(Asset asset) {
@@ -291,30 +283,21 @@ class GalleryViewerPage extends HookConsumerWidget {
color: Colors.black.withOpacity(0.4),
child: TopControlAppBar(
isPlayingMotionVideo: isPlayingMotionVideo.value,
asset: assetList[indexOfAsset.value],
isFavorite: ref.watch(favoriteProvider).contains(
assetList[indexOfAsset.value].id,
),
onMoreInfoPressed: () {
showInfo();
},
onFavorite: () {
toggleFavorite(assetList[indexOfAsset.value]);
},
onDownloadPressed: assetList[indexOfAsset.value].storage ==
AssetState.local
asset: asset(),
isFavorite: asset().isFavorite,
onMoreInfoPressed: showInfo,
onFavorite: asset().isRemote ? () => toggleFavorite(asset()) : null,
onDownloadPressed: asset().storage == AssetState.local
? null
: () {
: () =>
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
assetList[indexOfAsset.value],
asset(),
context,
);
},
),
onToggleMotionVideo: (() {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}),
onAddToAlbumPressed: () =>
addToAlbum(assetList[indexOfAsset.value]),
onAddToAlbumPressed: () => addToAlbum(asset()),
),
),
);
@@ -324,8 +307,6 @@ class GalleryViewerPage extends HookConsumerWidget {
final show = (showAppBar.value || // onTap has the final say
(showAppBar.value && !isZoomed.value)) &&
!isPlayingVideo.value;
final currentAsset = assetList[indexOfAsset.value];
return AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: show ? 1.0 : 0.0,
@@ -338,22 +319,26 @@ class GalleryViewerPage extends HookConsumerWidget {
showSelectedLabels: false,
showUnselectedLabels: false,
items: [
const BottomNavigationBarItem(
icon: Icon(Icons.ios_share_rounded),
label: 'Share',
tooltip: 'Share',
),
BottomNavigationBarItem(
icon: currentAsset.isArchived
? const Icon(Icons.unarchive_rounded)
: const Icon(Icons.archive_outlined),
label: 'Archive',
tooltip: 'Archive',
icon: const Icon(Icons.ios_share_rounded),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
const BottomNavigationBarItem(
icon: Icon(Icons.delete_outline),
label: 'Delete',
tooltip: 'Delete',
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) {
@@ -362,10 +347,10 @@ class GalleryViewerPage extends HookConsumerWidget {
shareAsset();
break;
case 1:
handleArchive(assetList[indexOfAsset.value]);
handleArchive(asset());
break;
case 2:
handleDelete(assetList[indexOfAsset.value]);
handleDelete(asset());
break;
}
},
@@ -395,33 +380,33 @@ class GalleryViewerPage extends HookConsumerWidget {
? const ScrollPhysics() // Use bouncing physics for iOS
: const ClampingScrollPhysics() // Use heavy physics for Android
),
itemCount: assetList.length,
itemCount: totalAssets,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
// Precache image
if (indexOfAsset.value < value) {
if (currentIndex.value < value) {
// Moving forwards, so precache the next asset
precacheNextImage(value + 1);
} else {
// Moving backwards, so precache previous asset
precacheNextImage(value - 1);
}
indexOfAsset.value = value;
currentIndex.value = value;
HapticFeedback.selectionClick();
},
loadingBuilder: isLoadPreview.value
? (context, event) {
final asset = assetList[indexOfAsset.value];
if (!asset.isLocal) {
final a = asset();
if (!a.isLocal) {
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
// Three-Stage Loading (WEBP -> JPEG -> Original)
final webPThumbnail = CachedNetworkImage(
imageUrl: getThumbnailUrl(
asset,
a,
type: api.ThumbnailFormat.WEBP,
),
cacheKey: getThumbnailCacheKey(
asset,
a,
type: api.ThumbnailFormat.WEBP,
),
httpHeaders: {'Authorization': authToken},
@@ -440,11 +425,11 @@ class GalleryViewerPage extends HookConsumerWidget {
// makes sense if the original is loaded in the builder
return CachedNetworkImage(
imageUrl: getThumbnailUrl(
asset,
a,
type: api.ThumbnailFormat.JPEG,
),
cacheKey: getThumbnailCacheKey(
asset,
a,
type: api.ThumbnailFormat.JPEG,
),
httpHeaders: {'Authorization': authToken},
@@ -458,30 +443,30 @@ class GalleryViewerPage extends HookConsumerWidget {
}
} else {
return Image(
image: localThumbnailImageProvider(asset),
image: localThumbnailImageProvider(a),
fit: BoxFit.contain,
);
}
}
: null,
builder: (context, index) {
getAssetExif();
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
final asset = loadAsset(index);
if (asset.isImage && !isPlayingMotionVideo.value) {
// Show photo
final ImageProvider provider;
if (assetList[index].isLocal) {
provider = localImageProvider(assetList[index]);
if (asset.isLocal) {
provider = localImageProvider(asset);
} else {
if (isLoadOriginal.value) {
provider = originalImageProvider(assetList[index]);
provider = originalImageProvider(asset);
} else if (isLoadPreview.value) {
provider = remoteThumbnailImageProvider(
assetList[index],
asset,
api.ThumbnailFormat.JPEG,
);
} else {
provider = remoteThumbnailImageProvider(
assetList[index],
asset,
api.ThumbnailFormat.WEBP,
);
}
@@ -495,13 +480,13 @@ class GalleryViewerPage extends HookConsumerWidget {
showAppBar.value = !showAppBar.value,
imageProvider: provider,
heroAttributes: PhotoViewHeroAttributes(
tag: assetList[index].id,
tag: asset.id,
),
filterQuality: FilterQuality.high,
tightMode: true,
minScale: PhotoViewComputedScale.contained,
errorBuilder: (context, error, stackTrace) => ImmichImage(
assetList[indexOfAsset.value],
asset,
fit: BoxFit.contain,
),
);
@@ -512,7 +497,7 @@ class GalleryViewerPage extends HookConsumerWidget {
onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details),
heroAttributes: PhotoViewHeroAttributes(
tag: assetList[index].id,
tag: asset.id,
),
filterQuality: FilterQuality.high,
maxScale: 1.0,
@@ -522,7 +507,7 @@ class GalleryViewerPage extends HookConsumerWidget {
child: VideoViewerPage(
onPlaying: () => isPlayingVideo.value = true,
onPaused: () => isPlayingVideo.value = false,
asset: assetList[index],
asset: asset,
isMotionVideo: isPlayingMotionVideo.value,
onVideoEnded: () {
if (isPlayingMotionVideo.value) {

View File

@@ -1,68 +1,25 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class FavoriteSelectionNotifier extends StateNotifier<Set<int>> {
FavoriteSelectionNotifier(this.assetsState, this.assetNotifier) : super({}) {
state = assetsState.allAssets
.where((asset) => asset.isFavorite)
.map((asset) => asset.id)
.toSet();
final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
final query = ref
.watch(dbProvider)
.assets
.filter()
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.isFavoriteEqualTo(true)
.sortByFileCreatedAt();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
yield await RenderList.fromQuery(query, groupBy);
await for (final _ in query.watchLazy()) {
yield await RenderList.fromQuery(query, groupBy);
}
final AssetsState assetsState;
final AssetNotifier assetNotifier;
void _setFavoriteForAssetId(int id, bool favorite) {
if (!favorite) {
state = state.difference({id});
} else {
state = state.union({id});
}
}
bool _isFavorite(int id) {
return state.contains(id);
}
Future<void> toggleFavorite(Asset asset) async {
// TODO support local favorite assets
if (asset.storage == AssetState.local) return;
_setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
await assetNotifier.toggleFavorite(
asset,
state.contains(asset.id),
);
}
Future<void> addToFavorites(Iterable<Asset> assets) {
state = state.union(assets.map((a) => a.id).toSet());
final futures = assets.map(
(a) => assetNotifier.toggleFavorite(
a,
true,
),
);
return Future.wait(futures);
}
}
final favoriteProvider =
StateNotifierProvider<FavoriteSelectionNotifier, Set<int>>((ref) {
return FavoriteSelectionNotifier(
ref.watch(assetProvider),
ref.watch(assetProvider.notifier),
);
});
final favoriteAssetProvider = StateProvider((ref) {
final favorites = ref.watch(favoriteProvider);
return ref
.watch(assetProvider)
.allAssets
.where((element) => favorites.contains(element.id))
.toList();
});

View File

@@ -1,36 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
class FavoriteImage extends HookConsumerWidget {
final Asset asset;
final List<Asset> assets;
const FavoriteImage(this.asset, this.assets, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
void viewAsset() {
AutoRouter.of(context).push(
GalleryViewerRoute(
asset: asset,
assetList: assets,
),
);
}
return GestureDetector(
onTap: viewAsset,
child: ImmichImage(
asset,
width: 300,
height: 300,
),
);
}
}

View File

@@ -1,15 +1,32 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class FavoritesPage extends HookConsumerWidget {
const FavoritesPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectionEnabledHook = useState(false);
final selection = useState(<Asset>{});
final processing = useState(false);
void selectionListener(
bool multiselect,
Set<Asset> selectedAssets,
) {
selectionEnabledHook.value = multiselect;
selection.value = selectedAssets;
}
AppBar buildAppBar() {
return AppBar(
leading: IconButton(
@@ -24,15 +41,77 @@ class FavoritesPage extends HookConsumerWidget {
);
}
void unfavorite() async {
try {
if (selection.value.isNotEmpty) {
await ref.watch(assetProvider.notifier).toggleFavorite(
selection.value.toList(),
false,
);
final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg:
'Removed ${selection.value.length} $assetOrAssets from favorites',
gravity: ToastGravity.CENTER,
);
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
Widget buildBottomBar() {
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 64,
child: Card(
child: Column(
children: [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
leading: const Icon(
Icons.star_border,
),
title: const Text(
"Unfavorite",
style: TextStyle(fontSize: 14),
),
onTap: processing.value ? null : unfavorite,
)
],
),
),
),
),
);
}
return Scaffold(
appBar: buildAppBar(),
body: ref.watch(favoriteAssetProvider).isEmpty
? const Center(
child: Text('No favorite assets found.'),
)
: ImmichAssetGrid(
assets: ref.watch(favoriteAssetProvider),
),
body: ref.watch(favoriteAssetsProvider).when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(child: Text(error.toString())),
data: (data) => data.isEmpty
? Center(
child: Text('favorites_page_no_favorites'.tr()),
)
: Stack(
children: [
ImmichAssetGrid(
renderList: data,
selectionActive: selectionEnabledHook.value,
listener: selectionListener,
),
if (selectionEnabledHook.value) buildBottomBar()
],
),
),
);
}
}

View File

@@ -2,212 +2,313 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
final log = Logger('AssetGridDataStructure');
enum RenderAssetGridElementType {
assets,
assetRow,
groupDividerTitle,
monthTitle;
}
class RenderAssetGridRow {
final List<Asset> assets;
final List<double> widthDistribution;
RenderAssetGridRow(this.assets, this.widthDistribution);
}
class RenderAssetGridElement {
final RenderAssetGridElementType type;
final RenderAssetGridRow? assetRow;
final String? title;
final DateTime date;
final List<Asset>? relatedAssetList;
final int count;
final int offset;
final int totalCount;
RenderAssetGridElement(
this.type, {
this.assetRow,
this.title,
required this.date,
this.relatedAssetList,
this.count = 0,
this.offset = 0,
this.totalCount = 0,
});
}
enum GroupAssetsBy {
day,
month;
}
class AssetGridLayoutParameters {
final int perRow;
final bool dynamicLayout;
final GroupAssetsBy groupBy;
AssetGridLayoutParameters(
this.perRow,
this.dynamicLayout,
this.groupBy,
);
}
class _AssetGroupsToRenderListComputeParameters {
final List<Asset> assets;
final AssetGridLayoutParameters layout;
_AssetGroupsToRenderListComputeParameters(
this.assets,
this.layout,
);
month,
auto,
none,
;
}
class RenderList {
final List<RenderAssetGridElement> elements;
final List<Asset>? allAssets;
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
final int totalAssets;
RenderList(this.elements);
/// reference to batch of assets loaded from DB with offset [_bufOffset]
List<Asset> _buf = [];
static Map<DateTime, List<Asset>> _groupAssets(
List<Asset> assets,
GroupAssetsBy groupBy,
) {
if (groupBy == GroupAssetsBy.day) {
return assets.groupListsBy(
(element) {
final date = element.fileCreatedAt.toLocal();
return DateTime(date.year, date.month, date.day);
},
);
} else if (groupBy == GroupAssetsBy.month) {
return assets.groupListsBy(
(element) {
final date = element.fileCreatedAt.toLocal();
return DateTime(date.year, date.month);
},
);
/// global offset of assets in [_buf]
int _bufOffset = 0;
RenderList(this.elements, this.query, this.allAssets)
: totalAssets = allAssets?.length ?? query!.countSync();
bool get isEmpty => totalAssets == 0;
/// Loads the requested assets from the database to an internal buffer if not cached
/// and returns a slice of that buffer
List<Asset> loadAssets(int offset, int count) {
assert(offset >= 0);
assert(count > 0);
assert(offset + count <= totalAssets);
if (allAssets != null) {
// if we already loaded all assets (e.g. from search result)
// simply return the requested slice of that array
return allAssets!.slice(offset, offset + count);
} else if (query != null) {
// general case: we have the query to load assets via offset from the DB on demand
if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) {
// the requested slice (offset:offset+count) is not contained in the cache buffer `_buf`
// thus, fill the buffer with a new batch of assets that at least contains the requested
// assets and some more
final bool forward = _bufOffset < offset;
// if the requested offset is greater than the cached offset, the user scrolls forward "down"
const batchSize = 256;
const oppositeSize = 64;
// make sure to load a meaningful amount of data (and not only the requested slice)
// otherwise, each call to [loadAssets] would result in DB call trashing performance
// fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests
final len = max(batchSize, count + oppositeSize);
// when scrolling forward, start shortly before the requested offset...
// when scrolling backward, end shortly after the requested offset...
// ... to guard against the user scrolling in the other direction
// a tiny bit resulting in a another required load from the DB
final start = max(
0,
forward
? offset - oppositeSize
: (len > batchSize ? offset : offset + count - len),
);
// load the calculated batch (start:start+len) from the DB and put it into the buffer
_buf = query!.offset(start).limit(len).findAllSync();
_bufOffset = start;
}
assert(_bufOffset <= offset);
assert(_bufOffset + _buf.length >= offset + count);
// return the requested slice from the buffer (we made sure before that the assets are loaded!)
return _buf.slice(offset - _bufOffset, offset - _bufOffset + count);
}
return {};
throw Exception("RenderList has neither assets nor query");
}
static Future<RenderList> _processAssetGroupData(
_AssetGroupsToRenderListComputeParameters data,
/// Returns the requested asset either from cached buffer or directly from the database
Asset loadAsset(int index) {
if (allAssets != null) {
// all assets are already loaded (e.g. from search result)
return allAssets![index];
} else if (query != null) {
// general case: we have the DB query to load asset(s) on demand
if (index >= _bufOffset && index < _bufOffset + _buf.length) {
// lucky case: the requested asset is already cached in the buffer!
return _buf[index - _bufOffset];
}
// request the asset from the database (not changing the buffer!)
final asset = query!.offset(index).findFirstSync();
if (asset == null) {
throw Exception(
"Asset at index $index does no longer exist in database",
);
}
return asset;
}
throw Exception("RenderList has neither assets nor query");
}
static Future<RenderList> fromQuery(
QueryBuilder<Asset, Asset, QAfterSortBy> query,
GroupAssetsBy groupBy,
) =>
_buildRenderList(null, query, groupBy);
static Future<RenderList> _buildRenderList(
List<Asset>? assets,
QueryBuilder<Asset, Asset, QAfterSortBy>? query,
GroupAssetsBy groupBy,
) async {
// TODO: Make DateFormat use the configured locale.
final monthFormat = DateFormat.yMMM();
final dayFormatSameYear = DateFormat.MMMEd();
final dayFormatOtherYear = DateFormat.yMMMEd();
final allAssets = data.assets;
final perRow = data.layout.perRow;
final dynamicLayout = data.layout.dynamicLayout;
final groupBy = data.layout.groupBy;
final List<RenderAssetGridElement> elements = [];
List<RenderAssetGridElement> elements = [];
DateTime? lastDate;
final groups = _groupAssets(allAssets, groupBy);
groups.entries.sortedBy((e) => e.key).reversed.forEach((entry) {
final date = entry.key;
final assets = entry.value;
try {
// Month title
if (groupBy == GroupAssetsBy.day &&
(lastDate == null || lastDate!.month != date.month)) {
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.monthTitle,
title: monthFormat.format(date),
date: date,
),
);
}
// Group divider title (day or month)
var formatDate = dayFormatOtherYear;
if (DateTime.now().year == date.year) {
formatDate = dayFormatSameYear;
}
if (groupBy == GroupAssetsBy.month) {
formatDate = monthFormat;
}
const pageSize = 500;
const sectionSize = 60; // divides evenly by 2,3,4,5,6
if (groupBy == GroupAssetsBy.none) {
final int total = assets?.length ?? query!.countSync();
for (int i = 0; i < total; i += sectionSize) {
final date = assets != null
? assets[i].fileCreatedAt
: await query!.offset(i).fileCreatedAtProperty().findFirst();
final int count = i + sectionSize > total ? total - i : sectionSize;
if (date == null) break;
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.groupDividerTitle,
title: formatDate.format(date),
RenderAssetGridElementType.assets,
date: date,
relatedAssetList: assets,
count: count,
totalCount: total,
offset: i,
),
);
// Add rows
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, perRow);
final rowAssets = assets.sublist(cursor, cursor + rowElements);
// Default: All assets have the same width
var widthDistribution = List.filled(rowElements, 1.0);
if (dynamicLayout) {
final aspectRatios =
rowAssets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList();
final meanAspectRatio = aspectRatios.sum / rowElements;
// 1: mean width
// 0.5: width < mean - threshold
// 1.5: width > mean + threshold
final arConfiguration = aspectRatios.map((e) {
if (e - meanAspectRatio > 0.3) return 1.5;
if (e - meanAspectRatio < -0.3) return 0.5;
return 1.0;
});
// Normalize:
final sum = arConfiguration.sum;
widthDistribution =
arConfiguration.map((e) => (e * rowElements) / sum).toList();
}
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
rowAssets,
widthDistribution,
),
);
elements.add(rowElement);
cursor += rowElements;
}
lastDate = date;
} catch (e, stackTrace) {
log.severe(e, stackTrace);
}
});
return RenderList(elements, query, assets);
}
return RenderList(elements);
final formatSameYear =
groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd();
final formatOtherYear = groupBy == GroupAssetsBy.month
? DateFormat.yMMMM()
: DateFormat.yMMMEd();
final currentYear = DateTime.now().year;
final formatMergedSameYear = DateFormat.MMMd();
final formatMergedOtherYear = DateFormat.yMMMd();
int offset = 0;
DateTime? last;
DateTime? current;
int lastOffset = 0;
int count = 0;
int monthCount = 0;
int lastMonthIndex = 0;
String formatDateRange(DateTime from, DateTime to) {
final startDate = (from.year == currentYear
? formatMergedSameYear
: formatMergedOtherYear)
.format(from);
final endDate = (to.year == currentYear
? formatMergedSameYear
: formatMergedOtherYear)
.format(to);
if (DateTime(from.year, from.month, from.day) ==
DateTime(to.year, to.month, to.day)) {
// format range with time when both dates are on the same day
final startTime = DateFormat.Hm().format(from);
final endTime = DateFormat.Hm().format(to);
return "$startDate $startTime - $endTime";
}
return "$startDate - $endDate";
}
void mergeMonth() {
if (last != null &&
groupBy == GroupAssetsBy.auto &&
monthCount <= 30 &&
elements.length > lastMonthIndex + 1) {
// merge all days into a single section
assert(elements[lastMonthIndex].date.month == last.month);
final e = elements[lastMonthIndex];
elements[lastMonthIndex] = RenderAssetGridElement(
RenderAssetGridElementType.monthTitle,
date: e.date,
count: monthCount,
totalCount: monthCount,
offset: e.offset,
title: formatDateRange(e.date, elements.last.date),
);
elements.removeRange(lastMonthIndex + 1, elements.length);
}
}
void addElems(DateTime d, DateTime? prevDate) {
final bool newMonth =
last == null || last.year != d.year || last.month != d.month;
if (newMonth) {
mergeMonth();
lastMonthIndex = elements.length;
monthCount = 0;
}
for (int j = 0; j < count; j += sectionSize) {
final type = j == 0
? (groupBy != GroupAssetsBy.month && newMonth
? RenderAssetGridElementType.monthTitle
: RenderAssetGridElementType.groupDividerTitle)
: (groupBy == GroupAssetsBy.auto
? RenderAssetGridElementType.groupDividerTitle
: RenderAssetGridElementType.assets);
final sectionCount = j + sectionSize > count ? count - j : sectionSize;
assert(sectionCount > 0 && sectionCount <= sectionSize);
elements.add(
RenderAssetGridElement(
type,
date: d,
count: sectionCount,
totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count,
offset: lastOffset + j,
title: j == 0
? (d.year == currentYear
? formatSameYear.format(d)
: formatOtherYear.format(d))
: (groupBy == GroupAssetsBy.auto
? formatDateRange(d, prevDate ?? d)
: null),
),
);
}
monthCount += count;
}
DateTime? prevDate;
while (true) {
// this iterates all assets (only their createdAt property) in batches
// memory usage is okay, however runtime is linear with number of assets
// TODO replace with groupBy once Isar supports such queries
final dates = assets != null
? assets.map((a) => a.fileCreatedAt)
: await query!
.offset(offset)
.limit(pageSize)
.fileCreatedAtProperty()
.findAll();
int i = 0;
for (final date in dates) {
final d = DateTime(
date.year,
date.month,
groupBy == GroupAssetsBy.month ? 1 : date.day,
);
current ??= d;
if (current != d) {
addElems(current, prevDate);
last = current;
current = d;
lastOffset = offset + i;
count = 0;
}
prevDate = date;
count++;
i++;
}
if (assets != null || dates.length != pageSize) break;
offset += pageSize;
}
if (count > 0 && current != null) {
addElems(current, prevDate);
mergeMonth();
}
assert(elements.every((e) => e.count <= sectionSize), "too large section");
return RenderList(elements, query, assets);
}
static RenderList empty() => RenderList([], null, []);
static Future<RenderList> fromAssets(
List<Asset> assets,
AssetGridLayoutParameters layout,
) async {
// Compute only allows for one parameter. Therefore we pass all parameters in a map
return compute(
_processAssetGroupData,
_AssetGroupsToRenderListComputeParameters(
assets,
layout,
),
);
}
GroupAssetsBy groupBy,
) =>
_buildRenderList(assets, null, groupBy);
}

View File

@@ -396,8 +396,8 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
widget.scrollStateListener(true);
dragHaltTimer = Timer(
const Duration(milliseconds: 200),
() {
const Duration(milliseconds: 500),
() {
widget.scrollStateListener(false);
},
);

View File

@@ -19,8 +19,6 @@ class GroupDividerTitle extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
void handleTitleIconClick() {
if (selected) {
onDeselect();
@@ -32,7 +30,7 @@ class GroupDividerTitle extends ConsumerWidget {
return Padding(
padding: const EdgeInsets.only(
top: 29.0,
bottom: 29.0,
bottom: 10.0,
left: 12.0,
right: 12.0,
),

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class ImmichAssetGrid extends HookConsumerWidget {
final int? assetsPerRow;
@@ -15,13 +16,19 @@ class ImmichAssetGrid extends HookConsumerWidget {
final bool? showStorageIndicator;
final ImmichAssetGridSelectionListener? listener;
final bool selectionActive;
final List<Asset> assets;
final List<Asset>? assets;
final RenderList? renderList;
final Future<void> Function()? onRefresh;
final Set<Asset>? preselectedAssets;
final bool canDeselect;
final bool? dynamicLayout;
final bool showMultiSelectIndicator;
final void Function(ItemPosition start, ItemPosition end)?
visibleItemsListener;
const ImmichAssetGrid({
super.key,
required this.assets,
this.assets,
this.onRefresh,
this.renderList,
this.assetsPerRow,
@@ -29,12 +36,16 @@ class ImmichAssetGrid extends HookConsumerWidget {
this.listener,
this.margin = 5.0,
this.selectionActive = false,
this.preselectedAssets,
this.canDeselect = true,
this.dynamicLayout,
this.showMultiSelectIndicator = true,
this.visibleItemsListener,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
var settings = ref.watch(appSettingsServiceProvider);
final renderListFuture = ref.watch(renderListProvider(assets));
// Needs to suppress hero animations when navigating to this widget
final enableHeroAnimations = useState(false);
@@ -64,34 +75,12 @@ class ImmichAssetGrid extends HookConsumerWidget {
return true;
}
if (renderList != null) {
Widget buildAssetGridView(RenderList renderList) {
return WillPopScope(
onWillPop: onWillPop,
child: HeroMode(
enabled: enableHeroAnimations.value,
child: ImmichAssetGridView(
allAssets: assets,
onRefresh: onRefresh,
assetsPerRow: assetsPerRow ??
settings.getSetting(AppSettingsEnum.tilesPerRow),
listener: listener,
showStorageIndicator: showStorageIndicator ??
settings.getSetting(AppSettingsEnum.storageIndicator),
renderList: renderList!,
margin: margin,
selectionActive: selectionActive,
),
),
);
}
return renderListFuture.when(
data: (renderList) => WillPopScope(
onWillPop: onWillPop,
child: HeroMode(
enabled: enableHeroAnimations.value,
child: ImmichAssetGridView(
allAssets: assets,
onRefresh: onRefresh,
assetsPerRow: assetsPerRow ??
settings.getSetting(AppSettingsEnum.tilesPerRow),
@@ -101,9 +90,22 @@ class ImmichAssetGrid extends HookConsumerWidget {
renderList: renderList,
margin: margin,
selectionActive: selectionActive,
preselectedAssets: preselectedAssets,
canDeselect: canDeselect,
dynamicLayout: dynamicLayout ??
settings.getSetting(AppSettingsEnum.dynamicLayout),
showMultiSelectIndicator: showMultiSelectIndicator,
visibleItemsListener: visibleItemsListener,
),
),
),
);
}
if (renderList != null) return buildAssetGridView(renderList!);
final renderListFuture = ref.watch(renderListProvider(assets!));
return renderListFuture.when(
data: (renderList) => buildAssetGridView(renderList),
error: (err, stack) => Center(child: Text("$err")),
loading: () => const Center(
child: ImmichLoadingIndicator(),

View File

@@ -1,4 +1,5 @@
import 'dart:collection';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/utils/builtin_extensions.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart';
import 'group_divider_title.dart';
@@ -23,13 +25,11 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
ItemPositionsListener.create();
bool _scrolling = false;
final Set<int> _selectedAssets = HashSet();
final Set<Asset> _selectedAssets =
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
Set<Asset> _getSelectedAssets() {
return _selectedAssets
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
.whereNotNull()
.toSet();
return Set.from(_selectedAssets);
}
void _callSelectionListener(bool selectionActive) {
@@ -38,18 +38,14 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
void _selectAssets(List<Asset> assets) {
setState(() {
for (var e in assets) {
_selectedAssets.add(e.id);
}
_selectedAssets.addAll(assets);
_callSelectionListener(true);
});
}
void _deselectAssets(List<Asset> assets) {
setState(() {
for (var e in assets) {
_selectedAssets.remove(e.id);
}
_selectedAssets.removeAll(assets);
_callSelectionListener(_selectedAssets.isNotEmpty);
});
}
@@ -57,64 +53,86 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
void _deselectAll() {
setState(() {
_selectedAssets.clear();
if (!widget.canDeselect &&
widget.preselectedAssets != null &&
widget.preselectedAssets!.isNotEmpty) {
_selectedAssets.addAll(widget.preselectedAssets!);
}
_callSelectionListener(false);
});
_callSelectionListener(false);
}
bool _allAssetsSelected(List<Asset> assets) {
return widget.selectionActive &&
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null;
}
Widget _buildThumbnailOrPlaceholder(
Asset asset,
bool placeholder,
) {
if (placeholder) {
return const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
);
}
Widget _buildThumbnailOrPlaceholder(Asset asset, int index) {
return ThumbnailImage(
asset: asset,
assetList: widget.allAssets,
index: index,
loadAsset: widget.renderList.loadAsset,
totalAssets: widget.renderList.totalAssets,
multiselectEnabled: widget.selectionActive,
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
isSelected: widget.selectionActive && _selectedAssets.contains(asset),
onSelect: () => _selectAssets([asset]),
onDeselect: () => _deselectAssets([asset]),
onDeselect: widget.canDeselect ||
widget.preselectedAssets == null ||
!widget.preselectedAssets!.contains(asset)
? () => _deselectAssets([asset])
: null,
useGrayBoxPlaceholder: true,
showStorageIndicator: widget.showStorageIndicator,
);
}
Widget _buildAssetRow(
Key key,
BuildContext context,
RenderAssetGridRow row,
bool scrolling,
List<Asset> assets,
int absoluteOffset,
double width,
) {
return LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth / widget.assetsPerRow -
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
return Row(
key: Key("asset-row-${row.assets.first.id}"),
children: row.assets.mapIndexed((int index, Asset asset) {
bool last = asset.id == row.assets.last.id;
// Default: All assets have the same width
final widthDistribution = List.filled(assets.length, 1.0);
return Container(
key: Key("asset-${asset.id}"),
width: size * row.widthDistribution[index],
height: size,
margin: EdgeInsets.only(
top: widget.margin,
right: last ? 0.0 : widget.margin,
),
child: _buildThumbnailOrPlaceholder(asset, scrolling),
);
}).toList(),
if (widget.dynamicLayout) {
final aspectRatios =
assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList();
final meanAspectRatio = aspectRatios.sum / assets.length;
// 1: mean width
// 0.5: width < mean - threshold
// 1.5: width > mean + threshold
final arConfiguration = aspectRatios.map((e) {
if (e - meanAspectRatio > 0.3) return 1.5;
if (e - meanAspectRatio < -0.3) return 0.5;
return 1.0;
});
// Normalize:
final sum = arConfiguration.sum;
widthDistribution.setRange(
0,
widthDistribution.length,
arConfiguration.map((e) => (e * assets.length) / sum),
);
}
return Row(
key: key,
children: assets.mapIndexed((int index, Asset asset) {
final bool last = index + 1 == widget.assetsPerRow;
return Container(
key: ValueKey(index),
width: width * widthDistribution[index],
height: width,
margin: EdgeInsets.only(
top: widget.margin,
right: last ? 0.0 : widget.margin,
),
child: _buildThumbnailOrPlaceholder(asset, absoluteOffset + index),
);
},
}).toList(),
);
}
@@ -132,10 +150,14 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
);
}
Widget _buildMonthTitle(BuildContext context, String title) {
Widget _buildMonthTitle(BuildContext context, DateTime date) {
final monthFormat = DateTime.now().year == date.year
? DateFormat.MMMM()
: DateFormat.yMMMM();
final String title = monthFormat.format(date);
return Padding(
key: Key("month-$title"),
padding: const EdgeInsets.only(left: 12.0, top: 32),
padding: const EdgeInsets.only(left: 12.0, top: 30),
child: Text(
title,
style: TextStyle(
@@ -147,18 +169,84 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
);
}
Widget _buildPlaceHolderRow(Key key, int num, double width, double height) {
return Row(
key: key,
children: [
for (int i = 0; i < num; i++)
Container(
key: ValueKey(i),
width: width,
height: height,
margin: EdgeInsets.only(
top: widget.margin,
right: i + 1 == num ? 0.0 : widget.margin,
),
color: Colors.grey,
)
],
);
}
Widget _buildSection(
BuildContext context,
RenderAssetGridElement section,
bool scrolling,
) {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth / widget.assetsPerRow -
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
final rows =
(section.count + widget.assetsPerRow - 1) ~/ widget.assetsPerRow;
final List<Asset> assetsToRender = scrolling
? []
: widget.renderList.loadAssets(section.offset, section.count);
return Column(
key: ValueKey(section.offset),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (section.type == RenderAssetGridElementType.monthTitle)
_buildMonthTitle(context, section.date),
if (section.type == RenderAssetGridElementType.groupDividerTitle ||
section.type == RenderAssetGridElementType.monthTitle)
_buildTitle(
context,
section.title!,
scrolling
? []
: widget.renderList
.loadAssets(section.offset, section.totalCount),
),
for (int i = 0; i < rows; i++)
scrolling
? _buildPlaceHolderRow(
ValueKey(i),
i + 1 == rows
? section.count - i * widget.assetsPerRow
: widget.assetsPerRow,
width,
width,
)
: _buildAssetRow(
ValueKey(i),
context,
assetsToRender.nestedSlice(
i * widget.assetsPerRow,
min((i + 1) * widget.assetsPerRow, section.count),
),
section.offset + i * widget.assetsPerRow,
width,
),
],
);
},
);
}
Widget _itemBuilder(BuildContext c, int position) {
final item = widget.renderList.elements[position];
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
return _buildTitle(c, item.title!, item.relatedAssetList!);
} else if (item.type == RenderAssetGridElementType.monthTitle) {
return _buildMonthTitle(c, item.title!);
} else if (item.type == RenderAssetGridElementType.assetRow) {
return _buildAssetRow(c, item.assetRow!, _scrolling);
}
return const Text("Invalid widget type!");
return _buildSection(c, item, _scrolling);
}
Text _labelBuilder(int pos) {
@@ -180,7 +268,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
}
Widget _buildAssetGrid() {
final useDragScrolling = widget.allAssets.length >= 20;
final useDragScrolling = widget.renderList.totalAssets >= 20;
void dragScrolling(bool active) {
setState(() {
@@ -225,6 +313,10 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
setState(() {
_selectedAssets.clear();
});
} else if (widget.preselectedAssets != null) {
setState(() {
_selectedAssets.addAll(widget.preselectedAssets!);
});
}
}
@@ -241,14 +333,33 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
void initState() {
super.initState();
scrollToTopNotifierProvider.addListener(_scrollToTop);
if (widget.visibleItemsListener != null) {
_itemPositionsListener.itemPositions.addListener(_positionListener);
}
}
@override
void dispose() {
scrollToTopNotifierProvider.removeListener(_scrollToTop);
if (widget.visibleItemsListener != null) {
_itemPositionsListener.itemPositions.removeListener(_positionListener);
}
super.dispose();
}
void _positionListener() {
final values = _itemPositionsListener.itemPositions.value;
final start = values.firstOrNull;
final end = values.lastOrNull;
if (start != null && end != null) {
if (start.index <= end.index) {
widget.visibleItemsListener?.call(start, end);
} else {
widget.visibleItemsListener?.call(end, start);
}
}
}
void _scrollToTop() {
// for some reason, this is necessary as well in order
// to correctly reposition the drag thumb scroll bar
@@ -268,7 +379,8 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
child: Stack(
children: [
_buildAssetGrid(),
if (widget.selectionActive) _buildMultiSelectIndicator(),
if (widget.showMultiSelectIndicator && widget.selectionActive)
_buildMultiSelectIndicator(),
],
),
);
@@ -282,19 +394,28 @@ class ImmichAssetGridView extends StatefulWidget {
final bool showStorageIndicator;
final ImmichAssetGridSelectionListener? listener;
final bool selectionActive;
final List<Asset> allAssets;
final Future<void> Function()? onRefresh;
final Set<Asset>? preselectedAssets;
final bool canDeselect;
final bool dynamicLayout;
final bool showMultiSelectIndicator;
final void Function(ItemPosition start, ItemPosition end)?
visibleItemsListener;
const ImmichAssetGridView({
super.key,
required this.renderList,
required this.allAssets,
required this.assetsPerRow,
required this.showStorageIndicator,
this.listener,
this.margin = 5.0,
this.selectionActive = false,
this.onRefresh,
this.preselectedAssets,
this.canDeselect = true,
this.dynamicLayout = true,
this.showMultiSelectIndicator = true,
this.visibleItemsListener,
});
@override

View File

@@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -10,7 +9,9 @@ import 'package:immich_mobile/utils/storage_indicator.dart';
class ThumbnailImage extends HookConsumerWidget {
final Asset asset;
final List<Asset> assetList;
final int index;
final Asset Function(int index) loadAsset;
final int totalAssets;
final bool showStorageIndicator;
final bool useGrayBoxPlaceholder;
final bool isSelected;
@@ -21,7 +22,9 @@ class ThumbnailImage extends HookConsumerWidget {
const ThumbnailImage({
Key? key,
required this.asset,
required this.assetList,
required this.index,
required this.loadAsset,
required this.totalAssets,
this.showStorageIndicator = true,
this.useGrayBoxPlaceholder = false,
this.isSelected = false,
@@ -57,8 +60,9 @@ class ThumbnailImage extends HookConsumerWidget {
} else {
AutoRouter.of(context).push(
GalleryViewerRoute(
assetList: assetList,
asset: asset,
initialIndex: index,
loadAsset: loadAsset,
totalAssets: totalAssets,
),
);
}
@@ -100,7 +104,9 @@ class ThumbnailImage extends HookConsumerWidget {
decoration: BoxDecoration(
border: multiselectEnabled && isSelected
? Border.all(
color: Theme.of(context).primaryColorLight,
color: onDeselect == null
? Colors.grey
: Theme.of(context).primaryColorLight,
width: 10,
)
: const Border(),
@@ -130,7 +136,7 @@ class ThumbnailImage extends HookConsumerWidget {
size: 18,
),
),
if (ref.watch(favoriteProvider).contains(asset.id))
if (asset.isFavorite)
const Positioned(
left: 10,
bottom: 5,

View File

@@ -7,15 +7,16 @@ import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/models/album.dart';
class ControlBottomAppBar extends ConsumerWidget {
final Function onShare;
final Function onFavorite;
final Function onArchive;
final Function onDelete;
final void Function() onShare;
final void Function() onFavorite;
final void Function() onArchive;
final void Function() onDelete;
final Function(Album album) onAddToAlbum;
final void Function() onCreateNewAlbum;
final List<Album> albums;
final List<Album> sharedAlbums;
final bool enabled;
const ControlBottomAppBar({
Key? key,
@@ -27,6 +28,7 @@ class ControlBottomAppBar extends ConsumerWidget {
required this.albums,
required this.onAddToAlbum,
required this.onCreateNewAlbum,
this.enabled = true,
}) : super(key: key);
@override
@@ -39,35 +41,31 @@ class ControlBottomAppBar extends ConsumerWidget {
ControlBoxButton(
iconData: Icons.ios_share_rounded,
label: "control_bottom_app_bar_share".tr(),
onPressed: () {
onShare();
},
onPressed: enabled ? onShare : null,
),
ControlBoxButton(
iconData: Icons.favorite_border_rounded,
label: "control_bottom_app_bar_favorite".tr(),
onPressed: () {
onFavorite();
},
onPressed: enabled ? onFavorite : null,
),
ControlBoxButton(
iconData: Icons.delete_outline_rounded,
label: "control_bottom_app_bar_delete".tr(),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return DeleteDialog(
onDelete: onDelete,
);
},
);
},
onPressed: enabled
? () => showDialog(
context: context,
builder: (BuildContext context) {
return DeleteDialog(
onDelete: onDelete,
);
},
)
: null,
),
ControlBoxButton(
iconData: Icons.archive,
label: "control_bottom_app_bar_archive".tr(),
onPressed: () => onArchive(),
onPressed: enabled ? onArchive : null,
),
],
);
@@ -108,7 +106,9 @@ class ControlBottomAppBar extends ConsumerWidget {
endIndent: 16,
thickness: 1,
),
AddToAlbumTitleRow(onCreateNewAlbum: onCreateNewAlbum),
AddToAlbumTitleRow(
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
),
],
),
),
@@ -118,6 +118,7 @@ class ControlBottomAppBar extends ConsumerWidget {
albums: albums,
sharedAlbums: sharedAlbums,
onAddToAlbum: onAddToAlbum,
enabled: enabled,
),
),
const SliverToBoxAdapter(
@@ -137,7 +138,7 @@ class AddToAlbumTitleRow extends StatelessWidget {
required this.onCreateNewAlbum,
});
final VoidCallback onCreateNewAlbum;
final VoidCallback? onCreateNewAlbum;
@override
Widget build(BuildContext context) {

View File

@@ -11,7 +11,7 @@ import 'package:immich_mobile/shared/models/server_info_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
class HomePageAppBar extends ConsumerWidget implements PreferredSizeWidget {
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);

View File

@@ -17,7 +17,6 @@ class ProfileDrawer extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
buildSignOutButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
height: double.infinity,
child: Icon(
@@ -48,7 +47,6 @@ class ProfileDrawer extends HookConsumerWidget {
buildSettingButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
height: double.infinity,
child: Icon(
@@ -72,7 +70,6 @@ class ProfileDrawer extends HookConsumerWidget {
buildAppLogButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
height: double.infinity,
child: Icon(

View File

@@ -10,14 +10,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
import 'package:immich_mobile/modules/home/ui/home_page_app_bar.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@@ -34,7 +31,6 @@ class HomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
final selectionEnabledHook = useState(false);
@@ -45,6 +41,7 @@ class HomePage extends HookConsumerWidget {
final tipOneOpacity = useState(0.0);
final refreshCount = useState(0);
final processing = useState(false);
useEffect(
() {
@@ -97,7 +94,7 @@ class HomePage extends HookConsumerWidget {
selectionEnabledHook.value = false;
}
Iterable<Asset> remoteOnlySelection({String? localErrorMessage}) {
List<Asset> remoteOnlySelection({String? localErrorMessage}) {
final Set<Asset> assets = selection.value;
final bool onlyRemote = assets.every((e) => e.isRemote);
if (!onlyRemote) {
@@ -108,113 +105,139 @@ class HomePage extends HookConsumerWidget {
gravity: ToastGravity.BOTTOM,
);
}
return assets.where((a) => a.isRemote);
return assets.where((a) => a.isRemote).toList();
}
return assets;
return assets.toList();
}
void onFavoriteAssets() {
final remoteAssets = remoteOnlySelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(),
);
if (remoteAssets.isNotEmpty) {
ref.watch(favoriteProvider.notifier).addToFavorites(remoteAssets);
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg: 'Added ${remoteAssets.length} $assetOrAssets to favorites',
gravity: ToastGravity.BOTTOM,
void onFavoriteAssets() async {
processing.value = true;
try {
final remoteAssets = remoteOnlySelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(),
);
}
if (remoteAssets.isNotEmpty) {
await ref
.watch(assetProvider.notifier)
.toggleFavorite(remoteAssets, true);
selectionEnabledHook.value = false;
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg: 'Added ${remoteAssets.length} $assetOrAssets to favorites',
gravity: ToastGravity.BOTTOM,
);
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
void onArchiveAsset() {
final remoteAssets = remoteOnlySelection(
localErrorMessage: 'home_page_archive_err_local'.tr(),
);
if (remoteAssets.isNotEmpty) {
ref.watch(assetProvider.notifier).toggleArchive(remoteAssets, true);
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
gravity: ToastGravity.CENTER,
void onArchiveAsset() async {
processing.value = true;
try {
final remoteAssets = remoteOnlySelection(
localErrorMessage: 'home_page_archive_err_local'.tr(),
);
}
if (remoteAssets.isNotEmpty) {
await ref
.watch(assetProvider.notifier)
.toggleArchive(remoteAssets, true);
selectionEnabledHook.value = false;
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
gravity: ToastGravity.CENTER,
);
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
void onDelete() {
ref.watch(assetProvider.notifier).deleteAssets(selection.value);
selectionEnabledHook.value = false;
void onDelete() async {
processing.value = true;
try {
await ref.watch(assetProvider.notifier).deleteAssets(selection.value);
selectionEnabledHook.value = false;
} finally {
processing.value = false;
}
}
void onAddToAlbum(Album album) async {
final Iterable<Asset> assets = remoteOnlySelection(
localErrorMessage: "home_page_add_to_album_err_local".tr(),
);
if (assets.isEmpty) {
return;
}
final result = await albumService.addAdditionalAssetToAlbum(
assets,
album,
);
if (result != null) {
if (result.alreadyInAlbum.isNotEmpty) {
ImmichToast.show(
context: context,
msg: "home_page_add_to_album_conflicts".tr(
namedArgs: {
"album": album.name,
"added": result.successfullyAdded.toString(),
"failed": result.alreadyInAlbum.length.toString()
},
),
);
} else {
ImmichToast.show(
context: context,
msg: "home_page_add_to_album_success".tr(
namedArgs: {
"album": album.name,
"added": result.successfullyAdded.toString(),
},
),
toastType: ToastType.success,
);
processing.value = true;
try {
final Iterable<Asset> assets = remoteOnlySelection(
localErrorMessage: "home_page_add_to_album_err_local".tr(),
);
if (assets.isEmpty) {
return;
}
final result = await albumService.addAdditionalAssetToAlbum(
assets,
album,
);
if (result != null) {
if (result.alreadyInAlbum.isNotEmpty) {
ImmichToast.show(
context: context,
msg: "home_page_add_to_album_conflicts".tr(
namedArgs: {
"album": album.name,
"added": result.successfullyAdded.toString(),
"failed": result.alreadyInAlbum.length.toString()
},
),
);
} else {
ImmichToast.show(
context: context,
msg: "home_page_add_to_album_success".tr(
namedArgs: {
"album": album.name,
"added": result.successfullyAdded.toString(),
},
),
toastType: ToastType.success,
);
}
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
void onCreateNewAlbum() async {
final Iterable<Asset> assets = remoteOnlySelection(
localErrorMessage: "home_page_add_to_album_err_local".tr(),
);
if (assets.isEmpty) {
return;
}
final result = await albumService.createAlbumWithGeneratedName(assets);
processing.value = true;
try {
final Iterable<Asset> assets = remoteOnlySelection(
localErrorMessage: "home_page_add_to_album_err_local".tr(),
);
if (assets.isEmpty) {
return;
}
final result =
await albumService.createAlbumWithGeneratedName(assets);
if (result != null) {
ref.watch(albumProvider.notifier).getAllAlbums();
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
selectionEnabledHook.value = false;
if (result != null) {
ref.watch(albumProvider.notifier).getAllAlbums();
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
selectionEnabledHook.value = false;
AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id));
AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id));
}
} finally {
processing.value = false;
}
}
Future<void> refreshAssets() async {
debugPrint("refreshCount.value ${refreshCount.value}");
final fullRefresh = refreshCount.value > 0;
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
if (fullRefresh) {
@@ -277,20 +300,18 @@ class HomePage extends HookConsumerWidget {
bottom: false,
child: Stack(
children: [
ref.watch(assetProvider).renderList == null ||
ref.watch(assetProvider).allAssets.isEmpty
? buildLoadingIndicator()
: ImmichAssetGrid(
renderList: ref.watch(assetProvider).renderList!,
assets: ref.read(assetProvider).allAssets,
assetsPerRow: appSettingService
.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator),
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
onRefresh: refreshAssets,
),
ref.watch(assetsProvider).when(
data: (data) => data.isEmpty
? buildLoadingIndicator()
: ImmichAssetGrid(
renderList: data,
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
onRefresh: refreshAssets,
),
error: (error, _) => Center(child: Text(error.toString())),
loading: buildLoadingIndicator,
),
if (selectionEnabledHook.value)
ControlBottomAppBar(
onShare: onShareAssets,
@@ -301,7 +322,9 @@ class HomePage extends HookConsumerWidget {
albums: albums,
sharedAlbums: sharedAlbums,
onCreateNewAlbum: onCreateNewAlbum,
enabled: !processing.value,
),
if (processing.value) const Center(child: ImmichLoadingIndicator())
],
),
);

View File

@@ -1,110 +0,0 @@
import 'dart:convert';
class LogInReponse {
final String accessToken;
final String userId;
final String userEmail;
final String firstName;
final String lastName;
final String profileImagePath;
final bool isAdmin;
final bool shouldChangePassword;
LogInReponse({
required this.accessToken,
required this.userId,
required this.userEmail,
required this.firstName,
required this.lastName,
required this.profileImagePath,
required this.isAdmin,
required this.shouldChangePassword,
});
LogInReponse copyWith({
String? accessToken,
String? userId,
String? userEmail,
String? firstName,
String? lastName,
String? profileImagePath,
bool? isAdmin,
bool? shouldChangePassword,
}) {
return LogInReponse(
accessToken: accessToken ?? this.accessToken,
userId: userId ?? this.userId,
userEmail: userEmail ?? this.userEmail,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
profileImagePath: profileImagePath ?? this.profileImagePath,
isAdmin: isAdmin ?? this.isAdmin,
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'accessToken': accessToken});
result.addAll({'userId': userId});
result.addAll({'userEmail': userEmail});
result.addAll({'firstName': firstName});
result.addAll({'lastName': lastName});
result.addAll({'profileImagePath': profileImagePath});
result.addAll({'isAdmin': isAdmin});
result.addAll({'shouldChangePassword': shouldChangePassword});
return result;
}
factory LogInReponse.fromMap(Map<String, dynamic> map) {
return LogInReponse(
accessToken: map['accessToken'] ?? '',
userId: map['userId'] ?? '',
userEmail: map['userEmail'] ?? '',
firstName: map['firstName'] ?? '',
lastName: map['lastName'] ?? '',
profileImagePath: map['profileImagePath'] ?? '',
isAdmin: map['isAdmin'] ?? false,
shouldChangePassword: map['shouldChangePassword'] ?? false,
);
}
String toJson() => json.encode(toMap());
factory LogInReponse.fromJson(String source) =>
LogInReponse.fromMap(json.decode(source));
@override
String toString() {
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is LogInReponse &&
other.accessToken == accessToken &&
other.userId == userId &&
other.userEmail == userEmail &&
other.firstName == firstName &&
other.lastName == lastName &&
other.profileImagePath == profileImagePath &&
other.isAdmin == isAdmin &&
other.shouldChangePassword == shouldChangePassword;
}
@override
int get hashCode {
return accessToken.hashCode ^
userId.hashCode ^
userEmail.hashCode ^
firstName.hashCode ^
lastName.hashCode ^
profileImagePath.hashCode ^
isAdmin.hashCode ^
shouldChangePassword.hashCode;
}
}

View File

@@ -9,13 +9,17 @@ import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/utils/db.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationNotifier(
this._apiService,
this._db,
) : super(
AuthenticationState(
deviceId: "",
@@ -31,6 +35,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
);
final ApiService _apiService;
final Isar _db;
Future<bool> login(
String email,
@@ -91,7 +96,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
try {
await Future.wait([
_apiService.authenticationApi.logout(),
Store.delete(StoreKey.assetETag),
clearAssetsAndAlbums(_db),
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
]);
@@ -170,5 +175,6 @@ final authenticationProvider =
StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
return AuthenticationNotifier(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
);
});

View File

@@ -4,8 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
SearchBar({
class ImmichSearchBar extends HookConsumerWidget
implements PreferredSizeWidget {
const ImmichSearchBar({
Key? key,
required this.searchFocusNode,
required this.onSubmitted,

View File

@@ -8,6 +8,8 @@ class SearchResultGrid extends HookConsumerWidget {
final List<Asset> assets;
Asset _loadAsset(int index) => assets[index];
@override
Widget build(BuildContext context, WidgetRef ref) {
return GridView.builder(
@@ -22,7 +24,9 @@ class SearchResultGrid extends HookConsumerWidget {
final asset = assets[index];
return ThumbnailImage(
asset: asset,
assetList: assets,
index: index,
loadAsset: _loadAsset,
totalAssets: assets.length,
useGrayBoxPlaceholder: true,
);
},

View File

@@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/ui/curated_row.dart';
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@@ -117,7 +117,7 @@ class SearchPage extends HookConsumerWidget {
}
return Scaffold(
appBar: SearchBar(
appBar: ImmichSearchBar(
searchFocusNode: searchFocusNode,
onSubmitted: onSearchSubmitted,
),

View File

@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
class LayoutSettings extends HookConsumerWidget {
const LayoutSettings({
@@ -22,14 +21,17 @@ class LayoutSettings extends HookConsumerWidget {
void switchChanged(bool value) {
appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value);
useDynamicLayout.value = value;
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
ref.invalidate(appSettingsServiceProvider);
}
void changeGroupValue(GroupAssetsBy? value) {
if (value != null) {
appSettingService.setSetting(AppSettingsEnum.groupAssetsBy, value.index);
appSettingService.setSetting(
AppSettingsEnum.groupAssetsBy,
value.index,
);
groupBy.value = value;
ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
ref.invalidate(appSettingsServiceProvider);
}
}
@@ -37,8 +39,8 @@ class LayoutSettings extends HookConsumerWidget {
() {
useDynamicLayout.value =
appSettingService.getSetting<bool>(AppSettingsEnum.dynamicLayout);
groupBy.value =
GroupAssetsBy.values[appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
groupBy.value = GroupAssetsBy.values[
appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
return null;
},
@@ -93,6 +95,19 @@ class LayoutSettings extends HookConsumerWidget {
onChanged: changeGroupValue,
controlAffinity: ListTileControlAffinity.trailing,
),
RadioListTile(
activeColor: Theme.of(context).primaryColor,
title: const Text(
"asset_list_layout_settings_group_automatically",
style: TextStyle(
fontSize: 12,
),
).tr(),
value: GroupAssetsBy.auto,
groupValue: groupBy.value,
onChanged: changeGroupValue,
controlAffinity: ListTileControlAffinity.trailing,
),
],
);
}

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