Compare commits

...

32 Commits

Author SHA1 Message Date
Alex The Bot
0f596e278c Version v1.88.0 2023-11-20 20:47:37 +00:00
Alex
74ad8b37bb docs: update requirement for CLI (#5198) 2023-11-20 19:54:46 +00:00
Jason Rasmussen
ec6b56f63c feat(format): bmp format (#5197) 2023-11-20 13:47:24 -06:00
Alex
1fbbb5a236 chore(web): album thumbnail size (#5196) 2023-11-20 13:22:35 -06:00
martin
725f30c494 fix: album sorting options (#5127)
* fix: album sort options

* fix: don't load assets

* pr feedback

* fix: albumStub

* fix(web): album shared without assets

* fix: tests

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-11-20 13:01:21 -06:00
Alex
347e6191c5 chore(mobile): minor font fix 2023-11-20 12:59:53 -06:00
Alex
94c8fe1098 chore(web): small font size improvement (#5190) 2023-11-20 11:23:47 -06:00
Alex
cb32b5cd7b chore(server): update new CLI into the image (#5192) 2023-11-20 11:23:28 -06:00
Mert
ddf04a7eb4 chore(ml): increase spool threshold to 64MiB (#5176) 2023-11-20 09:05:35 -06:00
Alex
acf099e481 chore(mobile): Mobile make over (#5129)
* chore: added overpass font

* Setting page

* style: app bar dialog

* style: backup controller and album selection page

* style: asset grid

* blanket fix

* blanket fix

* remove description input for local only asset

* revert

* merge main

* style: search page

* sharing page

* text size in sharing page

* style: library page

* library page

* album page + album creation page

* Navigationbar

* style: minor

* update

* album bottom sheet

* album option page

* minor style fix

* remove unused fonts

* remove fonts in pubspec
2023-11-20 08:58:03 -06:00
Mert
f7ada7351e update onnxruntime (#5175) 2023-11-20 08:44:45 -06:00
renovate[bot]
2a5cf20c9f chore(deps): update dependency eslint-config-prettier to v9 (#5173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-20 08:39:00 -06:00
Jonathan Jogenfors
81259115d1 chore(cli): set cli workdir in npm publish (#5185)
* chore: set cli release working dir

* chore: add repo url to npmjs

* chore: bump node setup to v4

* chore: normalize the github url
2023-11-20 13:36:17 +00:00
bo0tzz
f20a6cb321 chore(docs): Redirect old CLI paths (#5183)
* chore(docs): Redirect old CLI paths

* Update docs/vercel.json

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

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-11-20 13:20:58 +00:00
Jason Rasmussen
81af3b6f20 chore(cli): push to npm (#5168) 2023-11-20 14:04:20 +01:00
Jason Rasmussen
235b82b3fc chore(build): renovate grouping (#5167) 2023-11-20 09:19:10 +01:00
권중혁
e2317ea35e Add Korean README (#5128)
* docs: add Korean README and links

* chore: correction of some words
2023-11-19 21:10:59 -06:00
Alex
f5d73b0499 feat(web): new fonts (#5165)
* feat(web): new fonts

* remove old fonts and make default font size larger

* fine tunning
2023-11-19 21:06:16 -06:00
Jonathan Jogenfors
c1239a7337 docs: remove old CLI first (#5163) 2023-11-19 16:41:42 -06:00
Jonathan Jogenfors
7e38e7c949 feat(cli): refactor and add album support (#4434)
* Allow building and installing cli

* feat: add format fix

* docs: remove cli folder

* feat: use immich scoped package

* feat: rewrite cli readme

* docs: add info on running without building

* cleanup

* chore: remove import functionality from cli

* feat: add logout to cli

* docs: add todo for file format from server

* docs: add compilation step to cli

* fix: success message spacing

* feat: can create albums

* fix: add check step to cli

* fix: typos

* feat: pull file formats from server

* chore: use crawl service from server

* chore: fix lint

* docs: add cli documentation

* chore: rename ignore pattern

* chore: add version number to cli

* feat: use sdk

* fix: cleanup

* feat: album name on windows

* chore: remove skipped asset field

* feat: add more info to server-info command

* chore: cleanup

* chore: remove unneeded packages

* chore: fix docs links

* feat: add cli v2 milestone

* fix: set correct cli date

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-11-19 22:16:24 +00:00
digitaljamie
88b5f5b500 chore(docs): Update machine-learning.md 2023-11-19 18:47:42 +00:00
shenlong
983473261b refactor(mobile): riverpod codegen + riverpod lint (#4836)
* build(mobile): add riverpod_lint

* refactor(mobile): riverpod_generator for providers

* test(mobile): fix integration test helper

* refactor: ApiService to riverpod codegen

* refactor(mobile): return curatedcontent instead of people dto

* refactor: person provider to asyncnotifier

* mobile: update service providers to use lambda

* mobile: update scaffoldbody default error icon

* remove logger mixin

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2023-11-19 10:04:44 -06:00
Guillermo
bfab86b70d fix(web): improve year label position (#5141) 2023-11-19 09:31:06 -06:00
renovate[bot]
5b25d5140c chore(deps): update dependency @types/archiver to v6 (#5146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-19 09:29:51 -06:00
renovate[bot]
904756bbc5 chore(deps): update docker/build-push-action action to v5.1.0 (#5145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-19 08:34:43 -05:00
renovate[bot]
3a6e2c92cf chore(deps): update dependency eslint to v8.54.0 (#5140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-19 08:17:11 -05:00
renovate[bot]
4ade8eae17 chore(deps): update dependency @types/node to v20.9.2 (#5139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-19 05:23:21 +00:00
shenlong
41d43acf5f mobile: add initial DCM analysis_options (#5136)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2023-11-18 23:17:08 -06:00
renovate[bot]
fa71641ea4 chore(deps): update redis:6.2-alpine docker digest to 80cc851 (#5131)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-18 23:16:52 -06:00
shenlong
fce8d48de6 fix(mobile): use proper context for popping out from share (#5138)
* fix(mobile): use proper context for popping out from share

* mobile: use proper context for popping

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2023-11-18 23:13:38 -06:00
Michael Manganiello
6d310d6297 fix(mobile): Mark more strings for translation (#5132)
* fix(mobile): Mark more strings for translation

Moving more strings to the `i18n` JSON file, and also including their
es-US translations.

* Add more translatable strings
2023-11-18 20:32:28 -06:00
renovate[bot]
f5ce3deb3a fix(deps): update server (#5057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-18 15:59:04 -06:00
188 changed files with 2724 additions and 2152 deletions

2
.gitattributes vendored
View File

@@ -5,6 +5,8 @@ mobile/openapi/**/*.dart linguist-generated=true
mobile/openapi/.openapi-generator/FILES -diff -merge
mobile/openapi/.openapi-generator/FILES linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true
cli/src/api/open-api/**/*.md -diff -merge
cli/src/api/open-api/**/*.md linguist-generated=true

22
.github/workflows/cli-release.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Publish Package to npmjs
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./cli
steps:
- uses: actions/checkout@v2
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -96,7 +96,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v5.0.0
uses: docker/build-push-action@v5.1.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}

View File

@@ -32,3 +32,8 @@ jobs:
- name: Run dart analyze
run: dart analyze --fatal-infos
working-directory: ./mobile
# Enable after riverpod generator migration is completed
# - name: Run dart custom lint
# run: dart run custom_lint
# working-directory: ./mobile

View File

@@ -97,6 +97,10 @@ jobs:
run: npm run format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test:cov
if: ${{ !cancelled() }}

View File

@@ -18,14 +18,15 @@
</a>
<br/>
<p align="center">
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Disclaimer

View File

@@ -19,13 +19,14 @@
<br/>
<p align="center">
<a href="README.md">English</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Avís legal

View File

@@ -19,12 +19,14 @@
<br/>
<p align="center">
<a href="README.md">English</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Descargo de responsabilidad

View File

@@ -18,14 +18,15 @@
</a>
<br/>
<p align="center">
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README.md">English</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Clause de non-responsabilité

View File

@@ -19,13 +19,14 @@
<br/>
<p align="center">
<a href="README.md">English</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Declino di responsabilità

View File

@@ -18,13 +18,15 @@
</a>
<br/>
<p align="center">
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README.md">English</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## 免責事項

116
README_ko_KR.md Normal file
View File

@@ -0,0 +1,116 @@
<p align="center">
<br/>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
<a href="https://discord.gg/D8JsnBEuKb">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a>
<br/>
<br/>
</p>
<p align="center">
<img src="design/immich-logo.svg" width="150" title="Login With Custom URL">
</p>
<h3 align="center">Immich - 고성능 자체 호스팅 사진 및 동영상 백업 솔루션</h3>
<br/>
<a href="https://immich.app">
<img src="design/immich-screenshots.png" title="Main Screenshot">
</a>
<br/>
<p align="center">
<a href="README.md">English</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## 주의 사항
- ⚠️ 이 프로젝트는 **매우 활발히** 개발 중입니다.
- ⚠️ 버그 및 잦은 변경 사항이 있을 수 있습니다.
- ⚠️ **사진과 동영상을 저장하는 유일한 방법으로 사용하지 마세요.**
- ⚠️ 중요한 사진과 동영상을 위해 항상 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 백업 계획을 따르세요!
## 목차
- [공식 문서](https://immich.app/docs)
- [로드맵](https://github.com/orgs/immich-app/projects/1)
- [데모](#demo)
- [기능](#features)
- [소개](https://immich.app/docs/overview/introduction)
- [설치](https://immich.app/docs/install/requirements)
- [기여 가이드](https://immich.app/docs/overview/support-the-project)
- [프로젝트 지원](#support-the-project)
## 문서
설치 가이드를 포함한 주요 문서는 https://immich.app 에서 확인할 수 있습니다.
## 데모
https://demo.immich.app 에서 웹 데모를 체험할 수 있습니다.
모바일 앱의 경우 `서버 엔드포인트 URL``https://demo.immich.app`를 입력합니다.
```bash title="Demo Credential"
자격 증명
email: demo@immich.app
password: demo
```
```
사양: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
```
## 기능
| 기능 | 모바일 | 웹 |
| ------------------------------------ | ----- | ----- |
| 사진, 동영상 업로드 및 보기 | 예 | 예 |
| 앱을 열 때 자동으로 백업 | 예 | N/A |
| 백업용 앨범 선택 | 예 | N/A |
| 로컬 기기로 사진 및 동영상 다운로드 | 예 | 예 |
| 다른 사용자 추가 | 예 | 예 |
| 앨범 및 공유 앨범 | 예 | 예 |
| 스와이프/드래그 가능한 스크롤 바 | 예 | 예 |
| RAW 포맷 지원 | 예 | 예 |
| 메타데이터 보기 (EXIF, 위치) | 예 | 예 |
| 메타데이터, 사물, 얼굴 및 클립으로 검색 | 예 | 예 |
| 관리 기능 (사용자 관리) | 아니요 | 예 |
| 백그라운드 백업 | 예 | N/A |
| 가상 스크롤 | 예 | 예 |
| OAuth 지원 | 예 | 예 |
| API 키 | N/A | 예 |
| 라이브 포토/모션 포토 백업 및 재생 | 예 | 예 |
| 사용자 정의 스토리지 구조 | 예 | 예 |
| 모든 사용자와 공유 | 아니요 | 예 |
| 아카이브 및 즐겨찾기 | 예 |예|
| 글로벌 지도 | 예 | 예 |
| 특정 사용자와 공유 | 예 | 예 |
| 얼굴 인식 및 클러스터링 | 예 | 예 |
| 추억 (~년 전) | 예 | 예 |
| 오프라인 지원 | 예 | 아니요 |
| 읽기 전용 갤러리 | 예 | 예 |
| 사진 스택 | 예 | 예 |
## 프로젝트 지원
저는 이 프로젝트에 전념해왔고, 앞으로도 멈추지 않을 것입니다. 문서를 업데이트하고, 새로운 기능을 추가하고, 버그를 수정하려 합니다. 하지만 혼자서는 할 수 없습니다. 계속해서 나아갈 수 있는 추가적인 동기부여를 위해 당신의 도움이 필요합니다.
[selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 진행자가 말했듯이, 우리가 하고 있는 것은 대규모 프로젝트입니다. 언젠가는 이 일을 풀타임으로 하는 것을 희망하며, 이를 실현하기 위해 당신의 도움이 필요합니다.
만약 이에 동의하거나 이 앱을 장기간 사용하고자 한다면, 아래의 수단을 통해 이 프로젝트를 지원해 주세요.
### 후원
- GitHub 스폰서를 통한 [정기 후원](https://github.com/sponsors/alextran1502)
- GitHub 스폰서를 통한 [일시 후원](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz

View File

@@ -18,14 +18,15 @@
</a>
<br/>
<p align="center">
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README.md">English</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Disclaimer

View File

@@ -19,13 +19,14 @@
<br/>
<p align="center">
<a href="README.md">English</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_zh_CN.md">中文</a>
</p>
## Feragatname

View File

@@ -23,16 +23,16 @@
<p align="center">
<a href="README.md">English</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
</p>
## 免责声明
- ⚠️ 本项目正在 **非常活跃** 地开发中。

10
cli/.npmignore Normal file
View File

@@ -0,0 +1,10 @@
**/*.spec.js
.editorconfig
.eslintignore
.eslintrc.js
.prettierignore
.prettierrc
package-lock.json
testSetup.js
tsconfig.json
tsconfig.build.json

21
cli/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Hau Tran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,46 +1,19 @@
A command-line interface for interfacing with Immich
A command-line interface for interfacing with the self-hosted photo manager [Immich](https://immich.app/).
# Getting started
Please see the [Immich CLI documentation](https://immich.app/docs/features/command-line-interface).
$ ts-node cli/src
# For developers
To start using the CLI, you need to login with an API key first:
To run the Immich CLI from source, run the following in the cli folder:
$ ts-node cli/src login-key https://your-immich-instance/api your-api-key
$ npm run build
$ ts-node .
NOTE: This will store your api key under ~/.config/immich/auth.yml
You'll need ts-node, the easiest way to install it is to use npm:
Next, you can run commands:
$ npm i -g ts-node
$ ts-node cli/src server-info
You can also build and install the CLI using
When you're done, log out to remove the credentials from your filesystem
$ ts-node cli/src logout
# Usage
```
Usage: immich [options] [command]
Immich command line interface
Options:
-h, --help display help for command
Commands:
upload [options] [paths...] Upload assets
import [options] [paths...] Import existing assets
server-info Display server information
login-key [instanceUrl] [apiKey] Login using an API key
help [command] display help for command
```
# Todo
- Sidecar should check both .jpg.xmp and .xmp
- Sidecar check could be case-insensitive
# Known issues
- Upload can't use sdk due to multiple issues
$ npm run build
$ npm install -g .

114
cli/package-lock.json generated
View File

@@ -1,21 +1,25 @@
{
"name": "immich-cli",
"name": "@immich/cli",
"version": "2.0.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich-cli",
"name": "@immich/cli",
"version": "2.0.4",
"license": "MIT",
"dependencies": {
"axios": "^1.4.0",
"axios": "^1.6.2",
"byte-size": "^8.1.1",
"cli-progress": "^3.12.0",
"commander": "^11.0.0",
"form-data": "^4.0.0",
"glob": "^10.3.1",
"picomatch": "^2.3.1",
"systeminformation": "^5.18.4",
"yaml": "^2.3.1"
},
"bin": {
"immich": "dist/index.js"
},
"devDependencies": {
"@types/byte-size": "^8.1.0",
"@types/chai": "^4.3.5",
@@ -29,7 +33,7 @@
"@typescript-eslint/parser": "^5.48.1",
"chai": "^4.3.7",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^47.0.0",
@@ -798,9 +802,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
"integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
"integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1580,9 +1584,9 @@
}
},
"node_modules/@types/node": {
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"version": "20.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
"integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -1959,9 +1963,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -2646,15 +2650,15 @@
}
},
"node_modules/eslint": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
"integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
"integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3",
"@eslint/js": "8.53.0",
"@eslint/js": "8.54.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -2701,9 +2705,9 @@
}
},
"node_modules/eslint-config-prettier": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz",
"integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
@@ -4835,6 +4839,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
@@ -5621,31 +5626,6 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
"node_modules/systeminformation": {
"version": "5.21.17",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.17.tgz",
"integrity": "sha512-JZYRCbIjk3WuBV59A9/rTla2rROX+aAJ9uo2Z1dI+bjieORcukClN8rlM1zE9NYKpULSbaGc+KKct/870lO0DA==",
"os": [
"darwin",
"linux",
"win32",
"freebsd",
"openbsd",
"netbsd",
"sunos",
"android"
],
"bin": {
"systeminformation": "lib/cli.js"
},
"engines": {
"node": ">=8.0.0"
},
"funding": {
"type": "Buy me a coffee",
"url": "https://www.buymeacoffee.com/systeminfo"
}
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -6735,9 +6715,9 @@
}
},
"@eslint/js": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
"integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
"integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
"dev": true
},
"@humanwhocodes/config-array": {
@@ -7377,9 +7357,9 @@
}
},
"@types/node": {
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"version": "20.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
"integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
@@ -7624,9 +7604,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -8117,15 +8097,15 @@
"dev": true
},
"eslint": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
"integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
"integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3",
"@eslint/js": "8.53.0",
"@eslint/js": "8.54.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -8181,9 +8161,9 @@
}
},
"eslint-config-prettier": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz",
"integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
"dev": true,
"requires": {}
},
@@ -9742,7 +9722,8 @@
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"pirates": {
"version": "4.0.6",
@@ -10299,11 +10280,6 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
"systeminformation": {
"version": "5.21.17",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.17.tgz",
"integrity": "sha512-JZYRCbIjk3WuBV59A9/rTla2rROX+aAJ9uo2Z1dI+bjieORcukClN8rlM1zE9NYKpULSbaGc+KKct/870lO0DA=="
},
"test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",

View File

@@ -1,14 +1,19 @@
{
"name": "immich-cli",
"name": "@immich/cli",
"version": "2.0.3",
"description": "Command Line Interface (CLI) for Immich",
"main": "dist/index.js",
"bin": {
"immich": "./dist/index.js"
},
"license": "MIT",
"dependencies": {
"axios": "^1.4.0",
"axios": "^1.6.2",
"byte-size": "^8.1.1",
"cli-progress": "^3.12.0",
"commander": "^11.0.0",
"form-data": "^4.0.0",
"glob": "^10.3.1",
"picomatch": "^2.3.1",
"systeminformation": "^5.18.4",
"yaml": "^2.3.1"
},
"devDependencies": {
@@ -24,7 +29,7 @@
"@typescript-eslint/parser": "^5.48.1",
"chai": "^4.3.7",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^47.0.0",
@@ -42,10 +47,12 @@
"scripts": {
"build": "tsc --project tsconfig.build.json",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
"prepack": "yarn build ",
"prepack": "npm run build",
"test": "jest",
"test:cov": "jest --coverage",
"format": "prettier --check ."
"format": "prettier --check .",
"format:fix": "prettier --write .",
"check": "tsc --noEmit"
},
"jest": {
"clearMocks": true,
@@ -62,7 +69,15 @@
"collectCoverageFrom": [
"<rootDir>/src/**/*.(t|j)s"
],
"moduleNameMapper": {
"^@api(|/.*)$": "<rootDir>/src/api/$1"
},
"coverageDirectory": "./coverage",
"testEnvironment": "node"
},
"repository": {
"type": "git",
"url": "git+https://github.com/immich-app/immich.git",
"directory": "cli"
}
}

View File

@@ -1,3 +0,0 @@
// ./__mocks__/axios.js
import mockAxios from 'jest-mock-axios';
export default mockAxios;

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.87.0
* The version of the OpenAPI document: 1.88.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.87.0
* The version of the OpenAPI document: 1.88.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.87.0
* The version of the OpenAPI document: 1.88.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.87.0
* The version of the OpenAPI document: 1.88.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.87.0
* The version of the OpenAPI document: 1.88.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -9,7 +9,6 @@ import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
export abstract class BaseCommand {
protected sessionService!: SessionService;
protected immichApi!: ImmichApi;
protected deviceId!: string;
protected user!: UserResponseDto;
protected serverVersion!: ServerVersionResponseDto;

View File

@@ -1,15 +1,19 @@
import { BaseCommand } from '../cli/base-command';
export default class ServerInfo extends BaseCommand {
static description = 'Display server information';
static enableJsonFlag = true;
public async run() {
console.log('Getting server information');
await this.connect();
const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion();
console.log(versionInfo);
console.log(`Server is running version ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
const { data: supportedmedia } = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
console.log(`Supported image types: ${supportedmedia.image.map((extension) => extension.replace('.', ''))}`);
console.log(`Supported video types: ${supportedmedia.video.map((extension) => extension.replace('.', ''))}`);
const { data: statistics } = await this.immichApi.assetApi.getAssetStatistics();
console.log(`Images: ${statistics.images}, Videos: ${statistics.videos}, Total: ${statistics.total}`);
}
}

View File

@@ -1,43 +1,36 @@
import { BaseCommand } from '../cli/base-command';
import { CrawledAsset } from '../cores/models/crawled-asset';
import { CrawlService, UploadService } from '../services';
import * as si from 'systeminformation';
import FormData from 'form-data';
import { Asset } from '../cores/models/asset';
import { CrawlService } from '../services';
import { UploadOptionsDto } from '../cores/dto/upload-options-dto';
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
import cliProgress from 'cli-progress';
import byteSize from 'byte-size';
import { BaseCommand } from '../cli/base-command';
export default class Upload extends BaseCommand {
private crawlService = new CrawlService();
private uploadService!: UploadService;
deviceId!: string;
uploadLength!: number;
dryRun = false;
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
await this.connect();
const uuid = await si.uuid();
this.deviceId = uuid.os || 'CLI';
this.uploadService = new UploadService(this.immichApi.apiConfiguration);
const deviceId = 'CLI';
this.dryRun = options.dryRun;
const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video);
const crawlOptions = new CrawlOptionsDto();
crawlOptions.pathsToCrawl = paths;
crawlOptions.recursive = options.recursive;
crawlOptions.excludePatterns = options.excludePatterns;
crawlOptions.exclusionPatterns = options.exclusionPatterns;
const crawledFiles: string[] = await this.crawlService.crawl(crawlOptions);
const crawledFiles: string[] = await crawlService.crawl(crawlOptions);
if (crawledFiles.length === 0) {
console.log('No assets found, exiting');
return;
}
const assetsToUpload = crawledFiles.map((path) => new CrawledAsset(path));
const assetsToUpload = crawledFiles.map((path) => new Asset(path, deviceId));
const uploadProgress = new cliProgress.SingleBar(
{
@@ -58,117 +51,86 @@ export default class Upload extends BaseCommand {
totalSize += asset.fileSize;
}
const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data;
uploadProgress.start(totalSize, 0);
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
for (const asset of assetsToUpload) {
uploadProgress.update({
filename: asset.path,
});
try {
for (const asset of assetsToUpload) {
uploadProgress.update({
filename: asset.path,
});
try {
if (options.import) {
const importData = {
assetPath: asset.path,
sidecarPath: asset.sidecarPath,
deviceAssetId: asset.deviceAssetId,
deviceId: this.deviceId,
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
isFavorite: false,
isReadOnly: options.readOnly,
};
let skipUpload = false;
if (!options.skipHash) {
const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] };
if (!this.dryRun) {
await this.uploadService.import(importData);
}
} else {
await this.uploadAsset(asset, options.skipHash);
const checkResponse = await this.immichApi.assetApi.checkBulkUpload({
assetBulkUploadCheckDto,
});
skipUpload = checkResponse.data.results[0].action === 'reject';
}
} catch (error) {
uploadProgress.stop();
throw error;
}
sizeSoFar += asset.fileSize;
if (!asset.skipped) {
totalSizeUploaded += asset.fileSize;
uploadCounter++;
}
if (!skipUpload) {
if (!options.dryRun) {
const res = await this.immichApi.assetApi.uploadFile(asset.getUploadFileRequest());
uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
if (options.album && asset.albumName) {
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
if (!album) {
const res = await this.immichApi.albumApi.createAlbum({
createAlbumDto: { albumName: asset.albumName },
});
album = res.data;
existingAlbums.push(album);
}
await this.immichApi.albumApi.addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: [res.data.id] } });
}
}
totalSizeUploaded += asset.fileSize;
uploadCounter++;
}
sizeSoFar += asset.fileSize;
uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
}
} finally {
uploadProgress.stop();
}
uploadProgress.stop();
let messageStart;
if (this.dryRun) {
messageStart = 'Would have ';
if (options.dryRun) {
messageStart = 'Would have';
} else {
messageStart = 'Successfully ';
messageStart = 'Successfully';
}
if (options.import) {
console.log(`${messageStart} imported ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
if (uploadCounter === 0) {
console.log('All assets were already uploaded, nothing to do.');
} else {
if (uploadCounter === 0) {
console.log('All assets were already uploaded, nothing to do.');
console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
}
if (options.delete) {
if (options.dryRun) {
console.log(`Would now have deleted assets, but skipped due to dry run`);
} else {
console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
}
if (options.delete) {
if (this.dryRun) {
console.log(`Would now have deleted assets, but skipped due to dry run`);
} else {
console.log('Deleting assets that have been uploaded...');
const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
deletionProgress.start(crawledFiles.length, 0);
console.log('Deleting assets that have been uploaded...');
const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
deletionProgress.start(crawledFiles.length, 0);
for (const asset of assetsToUpload) {
if (!this.dryRun) {
await asset.delete();
}
deletionProgress.increment();
for (const asset of assetsToUpload) {
if (!options.dryRun) {
await asset.delete();
}
deletionProgress.stop();
console.log('Deletion complete');
deletionProgress.increment();
}
}
}
}
private async uploadAsset(asset: CrawledAsset, skipHash = false) {
await asset.readData();
let skipUpload = false;
if (!skipHash) {
const checksum = await asset.hash();
const checkResponse = await this.uploadService.checkIfAssetAlreadyExists(asset.path, checksum);
skipUpload = checkResponse.data.results[0].action === 'reject';
}
if (skipUpload) {
asset.skipped = true;
} else {
const uploadFormData = new FormData();
uploadFormData.append('deviceAssetId', asset.deviceAssetId);
uploadFormData.append('deviceId', this.deviceId);
uploadFormData.append('fileCreatedAt', asset.fileCreatedAt);
uploadFormData.append('fileModifiedAt', asset.fileModifiedAt);
uploadFormData.append('isFavorite', String(false));
uploadFormData.append('assetData', asset.assetData, { filename: asset.path });
if (asset.sidecarData) {
uploadFormData.append('sidecarData', asset.sidecarData, {
filename: asset.sidecarPath,
contentType: 'application/xml',
});
}
if (!this.dryRun) {
await this.uploadService.upload(uploadFormData);
deletionProgress.stop();
console.log('Deletion complete');
}
}
}

View File

@@ -1,59 +0,0 @@
// Check asset-upload.config.spec.ts for complete list
// TODO: we should get this list from the server via API in the future
// Videos
const videos = ['mp4', 'webm', 'mov', '3gp', 'avi', 'm2ts', 'mts', 'mpg', 'flv', 'mkv', 'wmv'];
// Images
const heic = ['heic', 'heif'];
const jpeg = ['jpg', 'jpeg'];
const png = ['png'];
const gif = ['gif'];
const tiff = ['tif', 'tiff'];
const webp = ['webp'];
const dng = ['dng'];
const other = [
'3fr',
'ari',
'arw',
'avif',
'cap',
'cin',
'cr2',
'cr3',
'crw',
'dcr',
'nef',
'erf',
'fff',
'iiq',
'jxl',
'k25',
'kdc',
'mrw',
'orf',
'ori',
'pef',
'psd',
'raf',
'raw',
'rwl',
'sr2',
'srf',
'srw',
'orf',
'ori',
'x3f',
];
export const ACCEPTED_FILE_EXTENSIONS = [
...videos,
...jpeg,
...png,
...heic,
...gif,
...tiff,
...webp,
...dng,
...other,
];

View File

@@ -1,6 +1,6 @@
export class CrawlOptionsDto {
pathsToCrawl!: string[];
recursive = false;
includeHidden = false;
excludePatterns!: string[];
recursive? = false;
includeHidden? = false;
exclusionPatterns?: string[];
}

View File

@@ -1,9 +1,9 @@
export class UploadOptionsDto {
recursive = false;
excludePatterns!: string[];
exclusionPatterns!: string[];
dryRun = false;
skipHash = false;
delete = false;
import = false;
readOnly = true;
album = false;
}

View File

@@ -1,2 +1 @@
export * from './constants';
export * from './models';

View File

@@ -0,0 +1,91 @@
import * as fs from 'fs';
import { basename } from 'node:path';
import crypto from 'crypto';
import { AssetApiUploadFileRequest } from 'src/api/open-api';
import Os from 'os';
export class Asset {
readonly path: string;
readonly deviceId!: string;
assetData?: File;
deviceAssetId?: string;
fileCreatedAt?: string;
fileModifiedAt?: string;
sidecarData?: File;
sidecarPath?: string;
fileSize!: number;
albumName?: string;
constructor(path: string, deviceId: string) {
this.path = path;
this.deviceId = deviceId;
}
async process() {
const stats = await fs.promises.stat(this.path);
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
this.fileCreatedAt = stats.mtime.toISOString();
this.fileModifiedAt = stats.mtime.toISOString();
this.fileSize = stats.size;
this.albumName = this.extractAlbumName();
this.assetData = await this.getFileObject(this.path);
// TODO: doesn't xmp replace the file extension? Will need investigation
const sideCarPath = `${this.path}.xmp`;
try {
fs.accessSync(sideCarPath, fs.constants.R_OK);
this.sidecarData = await this.getFileObject(sideCarPath);
} catch (error) {}
}
getUploadFileRequest(): AssetApiUploadFileRequest {
if (!this.assetData) throw new Error('Asset data not set');
if (!this.deviceAssetId) throw new Error('Device asset id not set');
if (!this.fileCreatedAt) throw new Error('File created at not set');
if (!this.fileModifiedAt) throw new Error('File modified at not set');
if (!this.deviceId) throw new Error('Device id not set');
return {
assetData: this.assetData,
deviceAssetId: this.deviceAssetId,
deviceId: this.deviceId,
fileCreatedAt: this.fileCreatedAt,
fileModifiedAt: this.fileModifiedAt,
isFavorite: false,
sidecarData: this.sidecarData,
};
}
private async getFileObject(path: string): Promise<File> {
const buffer = await fs.promises.readFile(path);
return new File([buffer], basename(path));
}
async delete(): Promise<void> {
return fs.promises.unlink(this.path);
}
public async hash(): Promise<string> {
const sha1 = (filePath: string) => {
const hash = crypto.createHash('sha1');
return new Promise<string>((resolve, reject) => {
const rs = fs.createReadStream(filePath);
rs.on('error', reject);
rs.on('data', (chunk) => hash.update(chunk));
rs.on('end', () => resolve(hash.digest('hex')));
});
};
return await sha1(this.path);
}
private extractAlbumName(): string {
if (Os.platform() === 'win32') {
return this.path.split('\\').slice(-2)[0];
} else {
return this.path.split('/').slice(-2)[0];
}
}
}

View File

@@ -1,58 +0,0 @@
import * as fs from 'fs';
import { basename } from 'node:path';
import crypto from 'crypto';
export class CrawledAsset {
public path: string;
public assetData?: fs.ReadStream;
public deviceAssetId?: string;
public fileCreatedAt?: string;
public fileModifiedAt?: string;
public sidecarData?: Buffer;
public sidecarPath?: string;
public fileSize!: number;
public skipped = false;
constructor(path: string) {
this.path = path;
}
async readData() {
this.assetData = fs.createReadStream(this.path);
}
async process() {
const stats = await fs.promises.stat(this.path);
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
this.fileCreatedAt = stats.mtime.toISOString();
this.fileModifiedAt = stats.mtime.toISOString();
this.fileSize = stats.size;
// TODO: doesn't xmp replace the file extension? Will need investigation
const sideCarPath = `${this.path}.xmp`;
try {
fs.accessSync(sideCarPath, fs.constants.R_OK);
this.sidecarData = await fs.promises.readFile(sideCarPath);
this.sidecarPath = sideCarPath;
} catch (error) {}
}
async delete(): Promise<void> {
return fs.promises.unlink(this.path);
}
public async hash(): Promise<string> {
const sha1 = (filePath: string) => {
const hash = crypto.createHash('sha1');
return new Promise<string>((resolve, reject) => {
const rs = fs.createReadStream(filePath);
rs.on('error', reject);
rs.on('data', (chunk) => hash.update(chunk));
rs.on('end', () => resolve(hash.digest('hex')));
});
};
return await sha1(this.path);
}
}

View File

@@ -1 +1 @@
export * from './crawled-asset';
export * from './asset';

View File

@@ -1,7 +1,10 @@
#! /usr/bin/env node
import { program, Option } from 'commander';
import Upload from './commands/upload';
import ServerInfo from './commands/server-info';
import LoginKey from './commands/login/key';
import Logout from './commands/logout';
program.name('immich').description('Immich command line interface');
@@ -12,6 +15,11 @@ program
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS'))
.addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
.addOption(
new Option('-a, --album', 'Automatically create albums based on folder name')
.env('IMMICH_AUTO_CREATE_ALBUM')
.default(false),
)
.addOption(
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
.env('IMMICH_DRY_RUN')
@@ -20,33 +28,13 @@ program
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
.argument('[paths...]', 'One or more paths to assets to be uploaded')
.action(async (paths, options) => {
options.excludePatterns = options.ignore;
await new Upload().run(paths, options);
});
program
.command('import')
.description('Import existing assets')
.usage('[options] [paths...]')
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
.addOption(
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
.env('IMMICH_DRY_RUN')
.default(false),
)
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false))
.addOption(new Option('--no-read-only', 'Import files without read-only protection, allowing Immich to manage them'))
.argument('[paths...]', 'One or more paths to assets to be imported')
.action(async (paths, options) => {
options.import = true;
options.excludePatterns = options.ignore;
options.exclusionPatterns = options.ignore;
await new Upload().run(paths, options);
});
program
.command('server-info')
.description('Display server information')
.action(async () => {
await new ServerInfo().run();
});
@@ -60,4 +48,11 @@ program
await new LoginKey().run(paths, options);
});
program
.command('logout')
.description('Remove stored credentials')
.action(async () => {
await new Logout().run();
});
program.parse(process.argv);

View File

@@ -1,235 +1,206 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { CrawlService } from './crawl.service';
import mockfs from 'mock-fs';
import { toIncludeSameMembers } from 'jest-extended';
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
import { CrawlService } from '.';
const matchers = require('jest-extended');
expect.extend(matchers);
interface Test {
test: string;
options: CrawlOptionsDto;
files: Record<string, boolean>;
}
const crawlService = new CrawlService();
const cwd = process.cwd();
describe('CrawlService', () => {
beforeAll(() => {
// Write a dummy output before mock-fs to prevent some annoying errors
console.log();
});
const tests: Test[] = [
{
test: 'should return empty when crawling an empty path list',
options: {
pathsToCrawl: [],
},
files: {},
},
{
test: 'should crawl a single path',
options: {
pathsToCrawl: ['/photos/'],
},
files: {
'/photos/image.jpg': true,
},
},
{
test: 'should exclude by file extension',
options: {
pathsToCrawl: ['/photos/'],
exclusionPatterns: ['**/*.tif'],
},
files: {
'/photos/image.jpg': true,
'/photos/image.tif': false,
},
},
{
test: 'should exclude by file extension without case sensitivity',
options: {
pathsToCrawl: ['/photos/'],
exclusionPatterns: ['**/*.TIF'],
},
files: {
'/photos/image.jpg': true,
'/photos/image.tif': false,
},
},
{
test: 'should exclude by folder',
options: {
pathsToCrawl: ['/photos/'],
exclusionPatterns: ['**/raw/**'],
},
files: {
'/photos/image.jpg': true,
'/photos/raw/image.jpg': false,
'/photos/raw2/image.jpg': true,
'/photos/folder/raw/image.jpg': false,
'/photos/crawl/image.jpg': true,
},
},
{
test: 'should crawl multiple paths',
options: {
pathsToCrawl: ['/photos/', '/images/', '/albums/'],
},
files: {
'/photos/image1.jpg': true,
'/images/image2.jpg': true,
'/albums/image3.jpg': true,
},
},
{
test: 'should support globbing paths',
options: {
pathsToCrawl: ['/photos*'],
},
files: {
'/photos1/image1.jpg': true,
'/photos2/image2.jpg': true,
'/images/image3.jpg': false,
},
},
{
test: 'should crawl a single path without trailing slash',
options: {
pathsToCrawl: ['/photos'],
},
files: {
'/photos/image.jpg': true,
},
},
{
test: 'should crawl a single path',
options: {
pathsToCrawl: ['/photos/'],
},
files: {
'/photos/image.jpg': true,
'/photos/subfolder/image1.jpg': true,
'/photos/subfolder/image2.jpg': true,
'/image1.jpg': false,
},
},
{
test: 'should filter file extensions',
options: {
pathsToCrawl: ['/photos/'],
},
files: {
'/photos/image.jpg': true,
'/photos/image.txt': false,
'/photos/1': false,
},
},
{
test: 'should include photo and video extensions',
options: {
pathsToCrawl: ['/photos/', '/videos/'],
},
files: {
'/photos/image.jpg': true,
'/photos/image.jpeg': true,
'/photos/image.heic': true,
'/photos/image.heif': true,
'/photos/image.png': true,
'/photos/image.gif': true,
'/photos/image.tif': true,
'/photos/image.tiff': true,
'/photos/image.webp': true,
'/photos/image.dng': true,
'/photos/image.nef': true,
'/videos/video.mp4': true,
'/videos/video.mov': true,
'/videos/video.webm': true,
},
},
{
test: 'should check file extensions without case sensitivity',
options: {
pathsToCrawl: ['/photos/'],
},
files: {
'/photos/image.jpg': true,
'/photos/image.Jpg': true,
'/photos/image.jpG': true,
'/photos/image.JPG': true,
'/photos/image.jpEg': true,
'/photos/image.TIFF': true,
'/photos/image.tif': true,
'/photos/image.dng': true,
'/photos/image.NEF': true,
},
},
{
test: 'should normalize the path',
options: {
pathsToCrawl: ['/photos/1/../2'],
},
files: {
'/photos/1/image.jpg': false,
'/photos/2/image.jpg': true,
},
},
{
test: 'should return absolute paths',
options: {
pathsToCrawl: ['photos'],
},
files: {
[`${cwd}/photos/1.jpg`]: true,
[`${cwd}/photos/2.jpg`]: true,
[`/photos/3.jpg`]: false,
},
},
];
it('should crawl a single directory', async () => {
mockfs({
'/photos/image.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
});
it('should crawl a single file', async () => {
mockfs({
'/photos/image.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/image.jpg'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
});
it('should crawl a file and a directory', async () => {
mockfs({
'/photos/image.jpg': '',
'/images/photo.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/image.jpg', '/images/'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/images/photo.jpg']);
});
it('should exclude by file extension', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/image.tif': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
options.excludePatterns = ['**/*.tif'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
});
it('should exclude by file extension without case sensitivity', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/image.tif': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
options.excludePatterns = ['**/*.TIF'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
});
it('should exclude by folder', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/raw/image.jpg': '',
'/photos/raw2/image.jpg': '',
'/photos/folder/raw/image.jpg': '',
'/photos/crawl/image.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
options.excludePatterns = ['**/raw/**'];
options.recursive = true;
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/photos/raw2/image.jpg', '/photos/crawl/image.jpg']);
});
it('should crawl multiple paths', async () => {
mockfs({
'/photos/image1.jpg': '',
'/images/image2.jpg': '',
'/albums/image3.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/', '/images/', '/albums/'];
options.recursive = false;
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image1.jpg', '/images/image2.jpg', '/albums/image3.jpg']);
});
it('should crawl a single path without trailing slash', async () => {
mockfs({
'/photos/image.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
});
it('should crawl a single path without recursion', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/subfolder/image1.jpg': '',
'/photos/subfolder/image2.jpg': '',
'/image1.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
});
it('should crawl a single path with recursion', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/subfolder/image1.jpg': '',
'/photos/subfolder/image2.jpg': '',
'/image1.jpg': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
options.recursive = true;
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers([
'/photos/image.jpg',
'/photos/subfolder/image1.jpg',
'/photos/subfolder/image2.jpg',
]);
});
it('should filter file extensions', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/image.txt': '',
'/photos/1': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
});
it('should include photo and video extensions', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/image.jpeg': '',
'/photos/image.heic': '',
'/photos/image.heif': '',
'/photos/image.png': '',
'/photos/image.gif': '',
'/photos/image.tif': '',
'/photos/image.tiff': '',
'/photos/image.webp': '',
'/photos/image.dng': '',
'/photos/image.nef': '',
'/videos/video.mp4': '',
'/videos/video.mov': '',
'/videos/video.webm': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/', '/videos/'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers([
'/photos/image.jpg',
'/photos/image.jpeg',
'/photos/image.heic',
'/photos/image.heif',
'/photos/image.png',
'/photos/image.gif',
'/photos/image.tif',
'/photos/image.tiff',
'/photos/image.webp',
'/photos/image.dng',
'/photos/image.nef',
'/videos/video.mp4',
'/videos/video.mov',
'/videos/video.webm',
]);
});
it('should check file extensions without case sensitivity', async () => {
mockfs({
'/photos/image.jpg': '',
'/photos/image.Jpg': '',
'/photos/image.jpG': '',
'/photos/image.JPG': '',
'/photos/image.jpEg': '',
'/photos/image.TIFF': '',
'/photos/image.tif': '',
'/photos/image.dng': '',
'/photos/image.NEF': '',
});
const options = new CrawlOptionsDto();
options.pathsToCrawl = ['/photos/'];
const paths: string[] = await crawlService.crawl(options);
expect(paths).toIncludeSameMembers([
'/photos/image.jpg',
'/photos/image.Jpg',
'/photos/image.jpG',
'/photos/image.JPG',
'/photos/image.jpEg',
'/photos/image.TIFF',
'/photos/image.tif',
'/photos/image.dng',
'/photos/image.NEF',
]);
});
describe(CrawlService.name, () => {
const sut = new CrawlService(
['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'],
['.mov', '.mp4', '.webm'],
);
afterEach(() => {
mockfs.restore();
});
describe('crawl', () => {
for (const { test, options, files } of tests) {
it(test, async () => {
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
const actual = await sut.crawl(options);
const expected = Object.entries(files)
.filter((entry) => entry[1])
.map(([file]) => file);
expect(actual.sort()).toEqual(expected.sort());
});
}
});
});

View File

@@ -1,47 +1,28 @@
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
import { ACCEPTED_FILE_EXTENSIONS } from '../cores';
import { glob } from 'glob';
import * as fs from 'fs';
export class CrawlService {
public async crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
const pathsToCrawl: string[] = crawlOptions.pathsToCrawl;
private readonly extensions!: string[];
const directories: string[] = [];
const crawledFiles: string[] = [];
constructor(image: string[], video: string[]) {
this.extensions = image.concat(video).map((extension) => extension.replace('.', ''));
}
for await (const currentPath of pathsToCrawl) {
const stats = await fs.promises.stat(currentPath);
if (stats.isFile() || stats.isSymbolicLink()) {
crawledFiles.push(currentPath);
} else {
directories.push(currentPath);
}
crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions;
if (!pathsToCrawl) {
return Promise.resolve([]);
}
let searchPattern: string;
if (directories.length === 1) {
searchPattern = directories[0];
} else if (directories.length === 0) {
return crawledFiles;
} else {
searchPattern = '{' + directories.join(',') + '}';
}
const base = pathsToCrawl.length === 1 ? pathsToCrawl[0] : `{${pathsToCrawl.join(',')}}`;
const extensions = `*{${this.extensions}}`;
if (crawlOptions.recursive) {
searchPattern = searchPattern + '/**/';
}
searchPattern = `${searchPattern}/*.{${ACCEPTED_FILE_EXTENSIONS.join(',')}}`;
const globbedFiles = await glob(searchPattern, {
return glob(`${base}/**/${extensions}`, {
absolute: true,
nocase: true,
nodir: true,
ignore: crawlOptions.excludePatterns,
dot: includeHidden,
ignore: exclusionPatterns,
});
const returnedFiles = crawledFiles.concat(globbedFiles);
returnedFiles.sort();
return returnedFiles;
}
}

View File

@@ -1,2 +1 @@
export * from './upload.service';
export * from './crawl.service';

View File

@@ -46,7 +46,7 @@ export class SessionService {
// Check if server and api key are valid
const { data: userInfo } = await this.api.userApi.getMyUserInfo().catch((error) => {
throw new LoginError(`Failed to connect to the server: ${error.message}`);
throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`);
});
console.log(`Logged in as ${userInfo.email}`);
@@ -78,7 +78,7 @@ export class SessionService {
private async ping(): Promise<void> {
const { data: pingResponse } = await this.api.serverInfoApi.pingServer().catch((error) => {
throw new Error(`Failed to connect to the server: ${error.message}`);
throw new Error(`Failed to connect to server ${this.api.apiConfiguration.instanceUrl}: ${error.message}`);
});
if (pingResponse.res !== 'pong') {

View File

@@ -1,24 +0,0 @@
import { UploadService } from './upload.service';
import axios from 'axios';
import FormData from 'form-data';
import { ApiConfiguration } from '../cores/api-configuration';
jest.mock('axios', () => jest.fn());
describe('UploadService', () => {
let uploadService: UploadService;
beforeEach(() => {
const apiConfiguration = new ApiConfiguration('https://example.com/api', 'key');
uploadService = new UploadService(apiConfiguration);
});
it('should call axios', async () => {
const data = new FormData();
await uploadService.upload(data);
expect(axios).toHaveBeenCalled();
});
});

View File

@@ -1,65 +0,0 @@
import axios, { AxiosRequestConfig } from 'axios';
import FormData from 'form-data';
import { ApiConfiguration } from '../cores/api-configuration';
export class UploadService {
private readonly uploadConfig: AxiosRequestConfig<any>;
private readonly checkAssetExistenceConfig: AxiosRequestConfig<any>;
private readonly importConfig: AxiosRequestConfig<any>;
constructor(apiConfiguration: ApiConfiguration) {
this.uploadConfig = {
method: 'post',
maxRedirects: 0,
url: `${apiConfiguration.instanceUrl}/asset/upload`,
headers: {
'x-api-key': apiConfiguration.apiKey,
},
maxContentLength: Number.POSITIVE_INFINITY,
maxBodyLength: Number.POSITIVE_INFINITY,
};
this.importConfig = {
method: 'post',
maxRedirects: 0,
url: `${apiConfiguration.instanceUrl}/asset/import`,
headers: {
'x-api-key': apiConfiguration.apiKey,
'Content-Type': 'application/json',
},
maxContentLength: Number.POSITIVE_INFINITY,
maxBodyLength: Number.POSITIVE_INFINITY,
};
this.checkAssetExistenceConfig = {
method: 'post',
maxRedirects: 0,
url: `${apiConfiguration.instanceUrl}/asset/bulk-upload-check`,
headers: {
'x-api-key': apiConfiguration.apiKey,
'Content-Type': 'application/json',
},
};
}
public checkIfAssetAlreadyExists(path: string, checksum: string) {
this.checkAssetExistenceConfig.data = JSON.stringify({ assets: [{ id: path, checksum: checksum }] });
// TODO: retry on 500 errors?
return axios(this.checkAssetExistenceConfig);
}
public upload(data: FormData) {
this.uploadConfig.data = data;
// TODO: retry on 500 errors?
return axios(this.uploadConfig);
}
public import(data: any) {
this.importConfig.data = data;
// TODO: retry on 500 errors?
return axios(this.importConfig);
}
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"strict": true,
"declaration": true,
"removeComments": true,
@@ -8,7 +8,7 @@
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"target": "es2017",
"target": "es2022",
"moduleResolution": "node16",
"sourceMap": true,
"outDir": "./dist",

View File

@@ -108,7 +108,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
database:
container_name: immich_postgres

View File

@@ -65,7 +65,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
restart: always
database:

View File

@@ -69,7 +69,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
restart: always
database:

View File

@@ -12,9 +12,9 @@ sidebar_position: 7
| ![cloud-cross](/img/cloud-off.svg) | Asset is only available locally and has not yet been backed up |
| ![cloud-done](/img/cloud-done.svg) | Asset was uploaded from this device and is now backed up in the cloud/server and still available in original on the device |
### How can I sync an existing directory with Immich's server?
### Can I add my existing photo library?
Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app/immich/discussions/1006)), but the [command line tool](/docs/features/bulk-upload.md) can bulk upload items from a directory to Immich.
Yes, with an [external library](/docs/features/libraries.md).
### Why are only photos and not videos being uploaded to Immich?

View File

@@ -34,7 +34,7 @@ The web app is a [TypeScript](https://www.typescriptlang.org/) project that uses
### CLI
The CLI is a [TypeScript](https://www.typescriptlang.org/) project that parses command line arguments to programmatically upload/import assets to an Immich server. See [Bulk Upload](/docs/features/bulk-upload.md) for more information about its usage.
The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/docs/features/command-line-interface.md) for more information.
## Server

View File

@@ -61,9 +61,15 @@ IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev
Setting these in the IDE give a better developer experience, auto-formatting code on save, and providing instant feedback on lint issues.
### Dart Code Metris
The mobile app uses DCM (Dart Code Metrics) for linting and metrics calculation. Please refer to the [Getting Started](https://dcm.dev/docs/getting-started/#installation) page for more information on setting up DCM
Note: Activating the license is not required.
### VSCode
Install `Flutter`, `Prettier`, `ESLint` and `Svelte` extensions.
Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions.
in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JSON`) add the following:

View File

@@ -1,113 +0,0 @@
# Bulk Upload (Using the CLI)
You can use the CLI to upload an existing gallery to the Immich server
[Immich CLI Repository](https://github.com/immich-app/CLI)
:::tip Google Photos Takeout
If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
:::
## Requirements
- Node.js 16 or above
- Npm
## Installation
```bash
npm i -g immich
```
Pre-installed on the `immich-server` container and can be easily accessed through
```
immich
```
### Options
| Parameter | Description |
| ---------------- | ------------------------------------------------------------------- |
| --yes / -y | Assume yes on all interactive prompts |
| --recursive / -r | Include subfolders |
| --delete / -da | Delete local assets after upload |
| --key / -k | User's API key |
| --server / -s | Immich's server address |
| --threads / -t | Number of threads to use (Default 5) |
| --album/ -al | Create albums for assets based on the parent folder or a given name |
## Quick Start
Specify user's credential, Immich's server address and port and the directory you would like to upload videos/photos from.
```
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api file1.jpg file2.jpg
```
By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option:
```
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive directory/
```
### Obtain the API Key
The API key can be obtained in the user setting panel on the web interface.
![Obtain Api Key](./img/obtain-api-key.png)
---
### Run via Docker
You can run the CLI inside of a docker container to avoid needing to install anything.
:::caution Running inside Docker
Be aware that as this runs inside a container, you need to mount the folder from which you want to import into the container.
:::
```bash title="Upload current directory"
cd /DIRECTORY/WITH/IMAGES
docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
```bash title="Upload target directory"
docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
```bash title="Create an alias"
alias immich='docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest'
immich upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
:::tip Internal networking
If you are running the CLI container on the same machine as your Immich server, you may not be able to reach the external address. In that case, try the following steps:
1. Find the internal Docker network used by Immich via `docker network ls`.
2. Adapt the above command to pass the `--network <immich_network>` argument to `docker run`, substituting `<immich_network>` with the result from step 1.
3. Use `--server http://immich-server:3001` for the upload command instead of the external address.
```bash title="Upload to internal address"
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://immich-server:3001
```
:::
### Run from source
```bash title="Clone Repository"
git clone https://github.com/immich-app/CLI
```
```bash title="Install dependencies"
npm install
```
```bash title="Build the project"
npm run build
```
```bash title="Run the command"
node bin/index.js upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive your/asset/directory
```

View File

@@ -0,0 +1,139 @@
# The Immich CLI
Immich has a CLI that allows you to perform certain actions from the command line. This CLI replaces the [legacy CLI](https://github.com/immich-app/CLI) that was previously available. The CLI is hosted in the [cli folder of the the main Immich github repository](https://github.com/immich-app/immich/tree/main/cli).
## Features
- Upload photos and videos to Immich
- Check server version
More features are planned for the future.
:::tip Google Photos Takeout
If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
:::
## Requirements
- Node.js 20.0 or above
- Npm
## Installation
```bash
npm i -g @immich/cli
```
NOTE: if you previously installed the legacy CLI, you will need to uninstall it first:
```bash
npm uninstall -g immich
```
## Usage
```
immich
```
```
Usage: immich [options] [command]
Immich command line interface
Options:
-h, --help display help for command
Commands:
upload [options] [paths...] Upload assets
server-info Display server information
login-key [instanceUrl] [apiKey] Login using an API key
logout Remove stored credentials
help [command] display help for command
```
## Commands
The upload command supports the following options:
```
Usage: immich upload [options] [paths...]
Upload assets
Arguments:
paths One or more paths to assets to be uploaded
Options:
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
-i, --ignore [paths...] Paths to ignore (env: IMMICH_IGNORE_PATHS)
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
--help display help for command
```
Note that the above options can read from environment variables as well.
## Quick Start
You begin by authenticating to your Immich server.
```bash
immich login-key [instanceUrl] [apiKey]
```
For instance,
```bash
immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG
```
This will store your credentials in a file in your home directory. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
Once you are authenticated, you can upload assets to your Immich server.
```bash
immich upload file1.jpg file2.jpg
```
By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option:
```bash
immich upload --recursive directory/
```
If you are unsure what will happen, you can use the `--dry-run` option to see what would happen without actually performing any actions.
```bash
immich upload --dry-run --recursive directory/
```
By default, the upload command will hash the files before uploading them. This is to avoid uploading the same file multiple times. If you are sure that the files are unique, you can skip this step by passing the `--skip-hash` option. Note that Immich always performs its own deduplication through hashing, so this is merely a performance consideration. If you have good bandwidth it might be faster to skip hashing.
```bash
immich upload --skip-hash --recursive directory/
```
You can automatically create albums based on the folder name by passing the `--album` option. This will automatically create albums for each uploaded asset based on the name of the folder they are in.
```bash
immich upload --album --recursive directory/
```
It is possible to skip assets matching a glob pattern by passing the `--ignore` option. See [the library documentation](docs/features/libraries.md) on how to use glob patterns. You can add several exclusion patterns if needed.
```bash
immich upload --ignore **/Raw/** --recursive directory/
```
```bash
immich upload --ignore **/Raw/** **/*.tif --recursive directory/
```
### Obtain the API Key
The API key can be obtained in the user setting panel on the web interface.
![Obtain Api Key](./img/obtain-api-key.png)

View File

@@ -4,10 +4,6 @@ import MobileAppBackup from '../partials/_mobile-app-backup.md';
# Mobile App
:::tip
To upload from other devices, try using the [Bulk Upload CLI](/docs/features/bulk-upload.md).
:::
## Download
<MobileAppDownload />

View File

@@ -2,7 +2,7 @@
To alleviate [performance issues on low-memory systems](/docs/FAQ.md#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer):
- Set `IMMICH_MACHINE_LEARNING_URL` to point to the designated ML system, e.g. `http://workstation:3003`.
- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
- Copy the following `docker-compose.yml` to your ML system.
- Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version).

View File

@@ -3,6 +3,7 @@ import {
mdiAndroid,
mdiAppleIos,
mdiArchiveOutline,
mdiBash,
mdiBookSearchOutline,
mdiCakeVariant,
mdiCheckAll,
@@ -49,6 +50,15 @@ import React from 'react';
import Timeline, { DateType, Item } from '../components/timeline';
const items: Item[] = [
{
icon: mdiBash,
description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.',
title: 'CLI v2',
release: 'v1.88.0',
tag: 'v1.88.0',
date: new Date(2023, 10, 19),
dateType: DateType.RELEASE,
},
{
icon: mdiStar,
description: 'Reach 20K Stars on GitHub!',

View File

@@ -12,7 +12,8 @@
{ "source": "/docs/overview/logo-meaning", "destination": "/docs/overview/logo" },
{ "source": "/docs/overview/technology-stack", "destination": "/docs/developer/architecture" },
{ "source": "/docs/usage/automatic-backup", "destination": "/docs/features/automatic-backup" },
{ "source": "/docs/usage/bulk-upload", "destination": "/docs/features/bulk-upload" },
{ "source": "/docs/usage/bulk-upload", "destination": "/docs/features/command-line-interface" },
{ "source": "/docs/features/bulk-upload", "destination": "/docs/features/command-line-interface" },
{ "source": "/docs/usage/oauth", "destination": "/docs/administration/oauth" },
{ "source": "/docs/usage/post-installation", "destination": "/docs/install/post-install" },
{ "source": "/docs/usage/update", "destination": "/docs/install/docker-compose#step-4---upgrading" },

View File

@@ -24,7 +24,7 @@ from .schemas import (
TextResponse,
)
MultiPartParser.max_file_size = 2**24 # spools to disk if payload is 16 MiB or larger
MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger
app = FastAPI()

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "aiocache"
@@ -2408,35 +2408,35 @@ reference = ["Pillow", "google-re2"]
[[package]]
name = "onnxruntime"
version = "1.16.1"
version = "1.16.2"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
{file = "onnxruntime-1.16.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:28b2c7f444b4119950b69370801cd66067f403d19cbaf2a444735d7c269cce4a"},
{file = "onnxruntime-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c24e04f33e7899f6aebb03ed51e51d346c1f906b05c5569d58ac9a12d38a2f58"},
{file = "onnxruntime-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fa93b166f2d97063dc9f33c5118c5729a4a5dd5617296b6dbef42f9047b3e81"},
{file = "onnxruntime-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042dd9201b3016ee18f8f8bc4609baf11ff34ca1ff489c0a46bcd30919bf883d"},
{file = "onnxruntime-1.16.1-cp310-cp310-win32.whl", hash = "sha256:c20aa0591f305012f1b21aad607ed96917c86ae7aede4a4dd95824b3d124ceb7"},
{file = "onnxruntime-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:5581873e578917bea76d6434ee7337e28195d03488dcf72d161d08e9398c6249"},
{file = "onnxruntime-1.16.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:ef8c0c8abf5f309aa1caf35941380839dc5f7a2fa53da533be4a3f254993f120"},
{file = "onnxruntime-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e680380bea35a137cbc3efd67a17486e96972901192ad3026ee79c8d8fe264f7"},
{file = "onnxruntime-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e62cc38ce1a669013d0a596d984762dc9c67c56f60ecfeee0d5ad36da5863f6"},
{file = "onnxruntime-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:025c7a4d57bd2e63b8a0f84ad3df53e419e3df1cc72d63184f2aae807b17c13c"},
{file = "onnxruntime-1.16.1-cp311-cp311-win32.whl", hash = "sha256:9ad074057fa8d028df248b5668514088cb0937b6ac5954073b7fb9b2891ffc8c"},
{file = "onnxruntime-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:d5e43a3478bffc01f817ecf826de7b25a2ca1bca8547d70888594ab80a77ad24"},
{file = "onnxruntime-1.16.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3aef4d70b0930e29a8943eab248cd1565664458d3a62b2276bd11181f28fd0a3"},
{file = "onnxruntime-1.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55a7b843a57c8ca0c8ff169428137958146081d5d76f1a6dd444c4ffcd37c3c2"},
{file = "onnxruntime-1.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c631af1941bf3b5f7d063d24c04aacce8cff0794e157c497e315e89ac5ad7b"},
{file = "onnxruntime-1.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671f296c3d5c233f601e97a10ab5a1dd8e65ba35c7b7b0c253332aba9dff330"},
{file = "onnxruntime-1.16.1-cp38-cp38-win32.whl", hash = "sha256:eb3802305023dd05e16848d4e22b41f8147247894309c0c27122aaa08793b3d2"},
{file = "onnxruntime-1.16.1-cp38-cp38-win_amd64.whl", hash = "sha256:fecfb07443d09d271b1487f401fbdf1ba0c829af6fd4fe8f6af25f71190e7eb9"},
{file = "onnxruntime-1.16.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:de3e12094234db6545c67adbf801874b4eb91e9f299bda34c62967ef0050960f"},
{file = "onnxruntime-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff723c2a5621b5e7103f3be84d5aae1e03a20621e72219dddceae81f65f240af"},
{file = "onnxruntime-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14a7fb3073aaf6b462e3d7fb433320f7700558a8892e5021780522dc4574292a"},
{file = "onnxruntime-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:963159f1f699b0454cd72fcef3276c8a1aab9389a7b301bcd8e320fb9d9e8597"},
{file = "onnxruntime-1.16.1-cp39-cp39-win32.whl", hash = "sha256:85771adb75190db9364b25ddec353ebf07635b83eb94b64ed014f1f6d57a3857"},
{file = "onnxruntime-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:d32d2b30799c1f950123c60ae8390818381fd5f88bdf3627eeca10071c155dc5"},
{file = "onnxruntime-1.16.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:e19316bb15c29ca0397e78861ee7cdb4db763ac5c53eaa83169bcdcb1149878c"},
{file = "onnxruntime-1.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:773f6d99d1e6a58936a55a4933c66674241dace9ec4bab71664cdfa170a7cd87"},
{file = "onnxruntime-1.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b8df9583a6e874f1983b85a361d22c205c96e926626eb486d3e69d72642f79"},
{file = "onnxruntime-1.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceef600de846997e3ef5f9af956ae87c88d84d6e925c3e9d435ce17ea223568f"},
{file = "onnxruntime-1.16.2-cp310-cp310-win32.whl", hash = "sha256:4fed41edb766c6adea6c34f1eb63a344d697fd4625133e5e48f23950bce60803"},
{file = "onnxruntime-1.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:9fc410ec220804fb384e7cb4fd68c474d89da11a1b68184db2001d64ba1477a9"},
{file = "onnxruntime-1.16.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:aa09d8d9d9a4dc2f6647b5135bb540da36e2d78206aaf14140ba73e05928c4f8"},
{file = "onnxruntime-1.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68f8d3347f11fcc6256266c562e4314b8c6da3e30fc275052a2ab693540b17fd"},
{file = "onnxruntime-1.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16217fa87d3482300a91036f9b499c85215a3b495de1ef9a68cbcf3df1a7c548"},
{file = "onnxruntime-1.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6b7046005442fcd09b86647bdc9a85d60c1367cb36ce7f16b942744cf27fe4"},
{file = "onnxruntime-1.16.2-cp311-cp311-win32.whl", hash = "sha256:773c231e526f815b8a3f3549d216cd8fed4c9e226e9e16e86af1b69a4bd29b58"},
{file = "onnxruntime-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:90e83a93b3d946c4a1d9dcbae286350accb0d80512d7c1b85953a444d19c0058"},
{file = "onnxruntime-1.16.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:8616f56905775dd8beeae11cf145542fff06c38cd97bfe9afe0c4a66142fc6d5"},
{file = "onnxruntime-1.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9f5e1d5ca5560044896edb2ad79113f863dc7daa804a26787c7b21c2a96d41e7"},
{file = "onnxruntime-1.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97ce538ffb668c4897e7500a586c150a045869876e0234e0611c4e4f428be63"},
{file = "onnxruntime-1.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cadf175baa782599f36586c23f84fe12b02702ceb59be57dbd8eefc6cc13cc4"},
{file = "onnxruntime-1.16.2-cp38-cp38-win32.whl", hash = "sha256:0ffd3b8a3039be713476b8783d254564976664c9b51ec70e7fb5d3e2832bf0f0"},
{file = "onnxruntime-1.16.2-cp38-cp38-win_amd64.whl", hash = "sha256:e2211f336e83819edbf174dcf56de35b0dcbfc6c92d3b685c8d85fba19bdf97d"},
{file = "onnxruntime-1.16.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:98a49bda980bcf819f8d9be880e3e7ba8a1df66aa5ce4fc7bb68ba9acf1fc7ad"},
{file = "onnxruntime-1.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f1e90fa0f43e988cd043e5a4b1eb77eda6cbd7523f316d93d36b33ff1ceb91f"},
{file = "onnxruntime-1.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0cbdb7df8078b2e8d9804de948963961eb8c6f417ef35ed243455162a9a065c"},
{file = "onnxruntime-1.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93c1cbd885c5fe0018b982c9dabe3cc3531416a3b50d0958a291605b32fe3ce"},
{file = "onnxruntime-1.16.2-cp39-cp39-win32.whl", hash = "sha256:713101b65d74438f380f5ea2475ce4f6026171e6229100e5be2baa92519fca17"},
{file = "onnxruntime-1.16.2-cp39-cp39-win_amd64.whl", hash = "sha256:3382934f9d86060b6bacd3eb4633c5ff904be2c99d3a7fb7faf2828381b15928"},
]
[package.dependencies]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.87.0"
version = "1.88.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -36,3 +36,58 @@ analyzer:
- openapi/
- openapi/test/
- lib/generated_plugin_registrant.dart
plugins:
- custom_lint
dart_code_metrics:
metrics:
cyclomatic-complexity: 20
number-of-parameters: 4
maximum-nesting-level: 5
rules:
# Common
- avoid-accessing-collections-by-constant-index
- avoid-accessing-other-classes-private-members
- avoid-async-call-in-sync-function
- avoid-cascade-after-if-null
- avoid-collapsible-if
- avoid-collection-methods-with-unrelated-types
- avoid-declaring-call-method
- avoid-double-slash-imports
- avoid-duplicate-cascades
- avoid-duplicate-patterns
- avoid-generics-shadowing
- avoid-global-state
# Flutter
- add-copy-with:
file-name-pattern: '.model.dart'
- always-remove-listener
- avoid-border-all
- avoid-empty-setstate
- avoid-expanded-as-spacer
- avoid-incomplete-copy-with
- avoid-inherited-widget-in-initstate
- avoid-late-context
- avoid-recursive-widget-calls
- avoid-returning-widgets
- avoid-shrink-wrap-in-lists
- avoid-single-child-column-or-row
- avoid-state-constructors
- avoid-stateless-widget-initialized-fields
- avoid-unnecessary-overrides-in-state
- avoid-unnecessary-stateful-widgets
- avoid-wrapping-in-padding
- dispose-fields
- prefer-const-border-radius
- prefer-correct-edge-insets-constructor
- prefer-dedicated-media-query-methods
- prefer-define-hero-tag
- prefer-extracting-callbacks
- prefer-single-widget-per-file:
ignore-private-widgets: true
- prefer-sliver-prefix
- prefer-text-rich
- prefer-using-list-view
- proper-super-calls
- use-setstate-synchronously

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 111,
"android.injected.version.name" => "1.87.0",
"android.injected.version.code" => 112,
"android.injected.version.name" => "1.88.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -1,6 +1,7 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
@@ -106,6 +107,9 @@
"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_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
@@ -195,6 +199,7 @@
"library_page_sort_title": "Album title",
"login_disabled": "Login has been disabled",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"login_form_back_button_text": "Back",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
@@ -217,6 +222,10 @@
"login_form_server_error": "Could not connect to server.",
"login_password_changed_error": "There was an error updating your password",
"login_password_changed_success": "Password updated successfully",
"map_assets_in_bounds": {
"one": "{} photo",
"other": "{} photos"
},
"map_cannot_get_user_location": "Cannot get user's location",
"map_location_dialog_cancel": "Cancel",
"map_location_dialog_yes": "Yes",
@@ -226,6 +235,15 @@
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
"map_no_location_permission_title": "Location Permission denied",
"map_settings_dark_mode": "Dark mode",
"map_settings_date_range_option_all": "All",
"map_settings_date_range_option_days": {
"one": "Past 24 hours",
"other": "Past {} days"
},
"map_settings_date_range_option_years": {
"one": "Past year",
"other": "Past {} years"
},
"map_settings_dialog_cancel": "Cancel",
"map_settings_dialog_save": "Save",
"map_settings_dialog_title": "Map Settings",
@@ -261,9 +279,13 @@
"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.",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_documentation": "Documentation",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
"profile_drawer_settings": "Settings",
"profile_drawer_sign_out": "Sign Out",
"profile_drawer_trash": "Trash",
@@ -275,6 +297,13 @@
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available",
"search_page_people": "People",
"search_page_person_add_name_dialog_cancel": "Cancel",
"search_page_person_add_name_dialog_save": "Save",
"search_page_person_add_name_dialog_hint": "Name",
"search_page_person_add_name_dialog_title": "Add a name",
"search_page_person_add_name_subtitle": "Find them fast by name with search",
"search_page_person_add_name_title": "Add a name",
"search_page_person_edit_name": "Edit name",
"search_page_places": "Places",
"search_page_recently_added": "Recently added",
"search_page_screenshots": "Screenshots",
@@ -283,6 +312,7 @@
"search_page_videos": "Videos",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_page_your_map": "Your Map",
"search_result_page_new_search_hint": "New Search",
"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",
@@ -322,9 +352,17 @@
"shared_album_activity_remove_title": "Delete Activity",
"shared_album_activity_setting_subtitle": "Let others respond",
"shared_album_activity_setting_title": "Comments & likes",
"shared_album_section_people_action_error": "Error leaving/removing from album",
"shared_album_section_people_action_leave": "Remove user from album",
"shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE",
"share_dialog_preparing": "Preparing...",
"shared_link_app_bar_title": "Shared Links",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_app_bar_title": "Create link to share",
"shared_link_create_error": "Error while creating shared link",
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
"shared_link_create_submit_button": "Create link",
"shared_link_edit_allow_download": "Allow public user to download",
@@ -334,6 +372,19 @@
"shared_link_edit_description": "Description",
"shared_link_edit_description_hint": "Enter the share description",
"shared_link_edit_expire_after": "Expire after",
"shared_link_edit_expire_after_option_days": {
"one": "{} day",
"other": "{} days"
},
"shared_link_edit_expire_after_option_hours": {
"one": "{} hour",
"other": "{} hours"
},
"shared_link_edit_expire_after_option_minutes": {
"one": "{} minute",
"other": "{} minutes"
},
"shared_link_edit_expire_after_option_never": "Never",
"shared_link_edit_password": "Password",
"shared_link_edit_password_hint": "Enter the share password",
"shared_link_edit_show_meta": "Show metadata",
@@ -345,7 +396,7 @@
"sharing_page_album": "Shared albums",
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
"sharing_page_empty_list": "EMPTY LIST",
"sharing_silver_appbar_create_shared_album": "Create shared album",
"sharing_silver_appbar_create_shared_album": "New shared album",
"sharing_silver_appbar_shared_links": "Shared links",
"sharing_silver_appbar_share_partner": "Share with partner",
"tab_controller_nav_library": "Library",
@@ -387,5 +438,6 @@
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack"
}
"viewer_unstack": "Un-Stack",
"scaffold_body_error_occured": "Error occured"
}

View File

@@ -1,6 +1,7 @@
{
"add_to_album_bottom_sheet_added": "Agregado a {album}",
"add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
"advanced_settings_log_level_title": "Nivel de registro: {}",
"advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
"advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
"advanced_settings_self_signed_ssl_subtitle": "Omite la verificación del certificado SSL para la URL del servidor. Requerido para certificados autofirmados.",
@@ -106,6 +107,9 @@
"cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} recursos)",
"cache_settings_clear_cache_button": "Borrar caché",
"cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.",
"cache_settings_duplicated_assets_clear_button": "BORRAR",
"cache_settings_duplicated_assets_subtitle": "Fotos y videos que son ignorados por la aplicación",
"cache_settings_duplicated_assets_title": "Recursos duplicados ({})",
"cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} recursos)",
"cache_settings_statistics_album": "Miniaturas de la biblioteca",
"cache_settings_statistics_assets": "{} recursos ({})",
@@ -174,6 +178,7 @@
"home_page_building_timeline": "Construyendo la línea de tiempo",
"home_page_favorite_err_local": "Aún no se pueden marcar recursos locales como favoritos, omitiendo",
"home_page_first_time_notice": "Si ésta 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.",
"home_page_share_err_local": "No se pueden compartir recursos locales a través de enlaces, omitiendo",
"home_page_upload_err_limit": "Sólo se pueden subir un máximo de 30 recursos a la vez, omitiendo",
"home_page_favorite_err_partner": "Aún no se pueden marcar recursos de compañeros como favoritos, omitiendo",
"home_page_album_err_partner": "Aún no se pueden agregar recursos de compañeros a un álbum, omitiendo",
@@ -194,6 +199,7 @@
"library_page_sort_title": "Título del álbum",
"login_disabled": "El inicio de sesión ha sido deshabilitado",
"login_form_api_exception": "Excepción de API. Por favor, verifica la URL del servidor e inténtalo de nuevo.",
"login_form_back_button_text": "Volver",
"login_form_button_text": "Iniciar sesión",
"login_form_email_hint": "tucorreo@correo.com",
"login_form_endpoint_hint": "http://ip-de-tu-servidor:puerto/api",
@@ -216,6 +222,10 @@
"login_form_server_error": "No se pudo conectar al servidor.",
"login_password_changed_error": "Hubo un error al actualizar tu contraseña",
"login_password_changed_success": "Contraseña actualizada exitosamente",
"map_assets_in_bounds": {
"one": "{} foto",
"other": "{} fotos"
},
"map_cannot_get_user_location": "No se puede obtener la ubicación del usuario",
"map_location_dialog_cancel": "Cancelar",
"map_location_dialog_yes": "Sí",
@@ -225,6 +235,15 @@
"map_no_location_permission_content": "Se necesita permiso de ubicación para mostrar recursos desde tu ubicación actual. ¿Quieres permitirlo ahora?",
"map_no_location_permission_title": "Permiso de ubicación denegado",
"map_settings_dark_mode": "Modo oscuro",
"map_settings_date_range_option_all": "Todo",
"map_settings_date_range_option_days": {
"one": "Últimas 24 horas",
"other": "Últimos {} días"
},
"map_settings_date_range_option_years": {
"one": "Último año",
"other": "Últimos {} años"
},
"map_settings_dialog_cancel": "Cancelar",
"map_settings_dialog_save": "Guardar",
"map_settings_dialog_title": "Configuración del mapa",
@@ -249,6 +268,7 @@
"partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos",
"partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?",
"partner_page_title": "Compañero",
"permission_onboarding_back": "Volver",
"permission_onboarding_continue_anyway": "Continuar de todos modos",
"permission_onboarding_get_started": "Empezar",
"permission_onboarding_go_to_settings": "Ir a configuración",
@@ -259,9 +279,13 @@
"permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.",
"permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.",
"profile_drawer_app_logs": "Registros",
"profile_drawer_client_out_of_date_major": "La aplicación móvil está desactualizada. Actualiza a la última versión mayor.",
"profile_drawer_client_out_of_date_minor": "La aplicación móvil está desactualizada. Actualiza a la última versión menor.",
"profile_drawer_client_server_up_to_date": "El cliente y el servidor están actualizados",
"profile_drawer_documentation": "Documentación",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "El servidor está desactualizado. Actualiza a la última versión mayor.",
"profile_drawer_server_out_of_date_minor": "El servidor está desactualizado. Actualiza a la última versión menor.",
"profile_drawer_settings": "Configuración",
"profile_drawer_sign_out": "Cerrar sesión",
"profile_drawer_trash": "Papelera",
@@ -269,10 +293,17 @@
"search_bar_hint": "Busca tus fotos",
"search_page_categories": "Categorías",
"search_page_favorites": "Favoritos",
"search_page_motion_photos": "Fotos en .ovimiento",
"search_page_motion_photos": "Fotos en movimiento",
"search_page_no_objects": "No hay información de objetos disponible",
"search_page_no_places": "No hay información de lugares disponible",
"search_page_people": "Personas",
"search_page_person_add_name_dialog_cancel": "Cancelar",
"search_page_person_add_name_dialog_hint": "Nombre",
"search_page_person_add_name_dialog_save": "Guardar",
"search_page_person_add_name_dialog_title": "Agregar nombre",
"search_page_person_add_name_subtitle": "Encuéntralos rápidamente por nombre",
"search_page_person_add_name_title": "Agregar un nombre",
"search_page_person_edit_name": "Editar nombre",
"search_page_places": "Lugares",
"search_page_recently_added": "Recién agregados",
"search_page_screenshots": "Capturas de pantalla",
@@ -281,6 +312,7 @@
"search_page_videos": "Videos",
"search_page_view_all_button": "Ver todo",
"search_page_your_activity": "Tu actividad",
"search_page_your_map": "Tu mapa",
"search_result_page_new_search_hint": "Nueva búsqueda",
"search_suggestion_list_smart_search_hint_1": "La búsqueda inteligente está habilitada por defecto, para buscar metadatos utiliza la sintaxis ",
"search_suggestion_list_smart_search_hint_2": "m:tu-término-de-búsqueda",
@@ -290,6 +322,7 @@
"server_info_box_app_version": "Versión de la Aplicación",
"server_info_box_server_url": "URL del Servidor",
"server_info_box_server_version": "Versión del Servidor",
"server_info_box_latest_release": "Última versión",
"setting_image_viewer_help": "El visor de detalles carga primero la miniatura pequeña, luego carga la vista previa de tamaño mediano (si está habilitada), finalmente carga la original (si está habilitada).",
"setting_image_viewer_original_subtitle": "Activar para cargar la imagen en resolución original (¡muy grande!). Deshabilitar para reducir el consumo de datos (de red y caché).",
"setting_image_viewer_original_title": "Cargar imagen original",
@@ -319,9 +352,17 @@
"shared_album_activity_remove_title": "Eliminar actividad",
"shared_album_activity_setting_subtitle": "Permitir que otros respondan",
"shared_album_activity_setting_title": "Comentarios y me gusta",
"shared_album_section_people_action_error": "Error al dejar/remover del álbum",
"shared_album_section_people_action_leave": "Dejar álbum",
"shared_album_section_people_action_remove_user": "Remover usuario del álbum",
"shared_album_section_people_owner_label": "Dueño",
"shared_album_section_people_title": "PERSONAS",
"share_dialog_preparing": "Preparando...",
"shared_link_app_bar_title": "Enlaces compartidos",
"shared_link_clipboard_copied_massage": "Copiado al portapapeles",
"shared_link_clipboard_text": "Enlace: {}\nContraseña: {}",
"shared_link_create_app_bar_title": "Crear enlace para compartir",
"shared_link_create_error": "Error al crear enlace compartido",
"shared_link_create_info": "Permitir que cualquiera con el enlace vea la(s) foto(s) seleccionada(s)",
"shared_link_create_submit_button": "Crear enlace",
"shared_link_edit_allow_download": "Permitir que el usuario público pueda descargar",
@@ -331,6 +372,19 @@
"shared_link_edit_description": "Descripción",
"shared_link_edit_description_hint": "Introduce la descripción del enlace",
"shared_link_edit_expire_after": "Expirar después de",
"shared_link_edit_expire_after_option_days": {
"one": "{} día",
"other": "{} días"
},
"shared_link_edit_expire_after_option_hours": {
"one": "{} hora",
"other": "{} horas"
},
"shared_link_edit_expire_after_option_minutes": {
"one": "{} minuto",
"other": "{} minutos"
},
"shared_link_edit_expire_after_option_never": "Nunca",
"shared_link_edit_password": "Contraseña",
"shared_link_edit_password_hint": "Introduce la contraseña del enlace",
"shared_link_edit_show_meta": "Mostrar metadatos",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,9 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:integration_test/integration_test.dart';
import 'package:isar/isar.dart';
// ignore: depend_on_referenced_packages
@@ -40,7 +42,12 @@ class ImmichTestHelper {
await Store.clear();
await db.writeTxn(() => db.clear());
// Load main Widget
await tester.pumpWidget(app.getMainWidget(db));
await tester.pumpWidget(
ProviderScope(
overrides: [dbProvider.overrideWithValue(db)],
child: app.getMainWidget(),
),
);
// Post run tasks
await EasyLocalization.ensureInitialized();
}

View File

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

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
import 'package:logging/logging.dart';
extension ScaffoldBody<T> on AsyncValue<T> {
static final Logger _scaffoldBodyLog = Logger("ScaffoldBody");
Widget scaffoldBodyWhen({
required Widget Function(T data) onData,
Widget? onError,
}) {
if (isLoading) {
return const Center(
child: ImmichLoadingIndicator(),
);
}
if (hasError && !hasValue) {
_scaffoldBodyLog.severe("Error occured in AsyncValue", error, stackTrace);
return onError ?? const ScaffoldErrorBody();
}
return onData(requireValue);
}
}

View File

@@ -43,7 +43,12 @@ void main() async {
await initApp();
await migrateDatabaseIfNeeded(db);
HttpOverrides.global = HttpSSLCertOverride();
runApp(getMainWidget(db));
runApp(
ProviderScope(
overrides: [dbProvider.overrideWithValue(db)],
child: getMainWidget(),
),
);
}
Future<void> initApp() async {
@@ -103,16 +108,13 @@ Future<Isar> loadDb() async {
return db;
}
Widget getMainWidget(Isar db) {
Widget getMainWidget() {
return EasyLocalization(
supportedLocales: locales,
path: translationsPath,
useFallbackTranslations: true,
fallbackLocale: locales.first,
child: ProviderScope(
overrides: [dbProvider.overrideWithValue(db)],
child: const ImmichApp(),
),
child: const ImmichApp(),
);
}

View File

@@ -16,7 +16,7 @@ class AlbumActionOutlinedButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
padding: const EdgeInsets.only(right: 16.0),
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
@@ -32,13 +32,13 @@ class AlbumActionOutlinedButton extends StatelessWidget {
),
icon: Icon(
iconData,
size: 15,
size: 18,
color: context.primaryColor,
),
label: Text(
labelText,
style: context.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
style: context.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
onPressed: onPressed,

View File

@@ -72,17 +72,13 @@ class AlbumThumbnailCard extends StatelessWidget {
.tr(args: ['${album.assetCount}'])
: 'album_thumbnail_card_items'
.tr(args: ['${album.assetCount}']),
style: TextStyle(
fontFamily: 'WorkSans',
fontSize: 12,
color: isDarkTheme ? Colors.white : Colors.black,
),
style: context.textTheme.bodyMedium,
),
if (owner != null) const TextSpan(text: ' · '),
if (owner != null)
TextSpan(
text: owner,
style: context.textTheme.labelSmall,
style: context.textTheme.bodyMedium,
),
],
),
@@ -114,11 +110,9 @@ class AlbumThumbnailCard extends StatelessWidget {
width: cardSize,
child: Text(
album.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isDarkTheme
? context.primaryColor
: Colors.black,
style: context.textTheme.bodyMedium?.copyWith(
color: context.primaryColor,
fontWeight: FontWeight.w500,
),
),
),

View File

@@ -188,7 +188,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
gravity: ToastGravity.BOTTOM,
);
}
context.pop();
buildContext.pop();
},
);
return const ShareDialog();
@@ -210,7 +210,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.ios_share_rounded),
title: const Text(
'album_viewer_appbar_share_to',
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onShareAssetsTo(),
),
@@ -219,7 +219,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.delete_sweep_rounded),
title: const Text(
'album_viewer_appbar_share_remove',
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onRemoveFromAlbumPressed(),
)
@@ -232,7 +232,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(
'album_viewer_appbar_share_delete',
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onDeleteAlbumPressed(),
)
@@ -240,7 +240,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
leading: const Icon(Icons.person_remove_rounded),
title: const Text(
'album_viewer_appbar_share_leave',
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: () => onLeaveAlbumPressed(),
),
@@ -258,7 +258,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
},
title: const Text(
"album_viewer_page_share_add_users",
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
ListTile(
@@ -269,7 +269,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
},
title: const Text(
"control_bottom_app_bar_share",
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
ListTile(
@@ -277,7 +277,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)),
title: const Text(
"translated_text_options",
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
];
@@ -291,7 +291,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
},
title: const Text(
"share_add_photos",
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
),
];

View File

@@ -44,7 +44,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
},
focusNode: titleFocusNode,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
style: context.textTheme.headlineMedium,
controller: titleTextEditController,
onTap: () {
FocusScope.of(context).requestFocus(titleFocusNode);

View File

@@ -30,7 +30,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
Navigator.pop(context);
ImmichToast.show(
context: context,
msg: "Error leaving/removing from album",
msg: "shared_album_section_people_action_error".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
@@ -81,7 +81,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
actions = [
ListTile(
leading: const Icon(Icons.exit_to_app_rounded),
title: const Text("Leave album"),
title: const Text("shared_album_section_people_action_leave").tr(),
onTap: leaveAlbum,
),
];
@@ -91,7 +91,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
actions = [
ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text("Remove user from album"),
title: const Text("shared_album_section_people_action_remove_user")
.tr(),
onTap: () => removeUserFromAlbum(user),
),
];
@@ -122,19 +123,17 @@ class AlbumOptionsPage extends HookConsumerWidget {
title: Text(
album.owner.value?.name ?? "",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
album.owner.value?.email ?? "",
style: TextStyle(color: Colors.grey[500]),
),
trailing: const Text(
"Owner",
style: TextStyle(
fontWeight: FontWeight.bold,
),
style: TextStyle(color: Colors.grey[600]),
),
trailing: Text(
"shared_album_section_people_owner_label",
style: context.textTheme.labelLarge,
).tr(),
);
}
@@ -152,12 +151,12 @@ class AlbumOptionsPage extends HookConsumerWidget {
title: Text(
user.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
user.email,
style: TextStyle(color: Colors.grey[500]),
style: TextStyle(color: Colors.grey[600]),
),
trailing: userId == user.id || isOwner
? const Icon(Icons.more_horiz_rounded)
@@ -209,13 +208,17 @@ class AlbumOptionsPage extends HookConsumerWidget {
dense: true,
title: Text(
"shared_album_activity_setting_title",
style: context.textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.bold),
style: context.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w500),
).tr(),
subtitle: Text(
"shared_album_activity_setting_subtitle",
style: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withAlpha(175),
),
).tr(),
subtitle:
const Text("shared_album_activity_setting_subtitle").tr(),
),
buildSectionTitle("PEOPLE"),
buildSectionTitle("shared_album_section_people_title".tr()),
buildOwnerInfo(),
buildSharedUsersList(),
],

View File

@@ -153,10 +153,7 @@ class AlbumViewerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 8.0),
child: Text(
album.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
style: context.textTheme.headlineMedium,
),
),
);
@@ -191,10 +188,7 @@ class AlbumViewerPage extends HookConsumerWidget {
),
child: Text(
dateRangeText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
style: context.textTheme.labelLarge,
),
);
}

View File

@@ -94,10 +94,7 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 200, left: 18),
child: Text(
'create_shared_album_page_share_add_assets',
style: context.textTheme.displayMedium?.copyWith(
fontSize: 12,
fontWeight: FontWeight.normal,
),
style: context.textTheme.labelLarge,
).tr(),
),
);
@@ -119,7 +116,7 @@ class CreateAlbumPage extends HookConsumerWidget {
side: BorderSide(
color: context.isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
: const Color.fromARGB(255, 129, 129, 129),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
@@ -134,9 +131,8 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'create_shared_album_page_share_select_photos',
style: context.textTheme.labelLarge?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
style: context.textTheme.titleMedium?.copyWith(
color: context.primaryColor,
),
).tr(),
),
@@ -222,11 +218,8 @@ class CreateAlbumPage extends HookConsumerWidget {
},
icon: const Icon(Icons.close_rounded),
),
title: Text(
title: const Text(
'share_create_album',
style: context.textTheme.displayMedium?.copyWith(
color: context.primaryColor,
),
).tr(),
actions: [
if (isSharedAlbum)

View File

@@ -125,10 +125,8 @@ class LibraryPage extends HookConsumerWidget {
),
Text(
options[selectedAlbumSortOrder.value],
style: TextStyle(
fontWeight: FontWeight.bold,
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
fontSize: 12.0,
),
),
],
@@ -172,11 +170,9 @@ class LibraryPage extends HookConsumerWidget {
top: 8.0,
bottom: 16,
),
child: const Text(
child: Text(
'library_page_new_album',
style: TextStyle(
fontWeight: FontWeight.bold,
),
style: context.textTheme.labelLarge,
).tr(),
),
],
@@ -198,9 +194,9 @@ class LibraryPage extends HookConsumerWidget {
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13.0,
color: isDarkTheme ? Colors.white : Colors.grey[800],
color: context.isDarkTheme
? Colors.white
: Colors.black.withAlpha(200),
),
),
),
@@ -278,9 +274,11 @@ class LibraryPage extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
Text(
'library_page_albums',
style: TextStyle(fontWeight: FontWeight.bold),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
).tr(),
buildSortButton(),
],
@@ -326,9 +324,11 @@ class LibraryPage extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
Text(
'library_page_device_albums',
style: TextStyle(fontWeight: FontWeight.bold),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
).tr(),
],
),

View File

@@ -80,25 +80,20 @@ class SharingPage extends HookConsumerWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color:
context.isDarkTheme ? context.primaryColor : Colors.black,
color: context.primaryColor,
fontWeight: FontWeight.w500,
),
),
subtitle: isOwner
? Text(
'album_thumbnail_owned'.tr(),
style: const TextStyle(
fontSize: 12.0,
),
style: context.textTheme.bodyMedium,
)
: album.ownerName != null
? Text(
'album_thumbnail_shared_by'
.tr(args: [album.ownerName!]),
style: const TextStyle(
fontSize: 12.0,
),
style: context.textTheme.bodyMedium,
)
: null,
onTap: () {
@@ -137,8 +132,8 @@ class SharingPage extends HookConsumerWidget {
"sharing_silver_appbar_create_shared_album",
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
fontWeight: FontWeight.w500,
fontSize: 12,
),
).tr(),
),
@@ -154,8 +149,8 @@ class SharingPage extends HookConsumerWidget {
label: const Text(
"sharing_silver_appbar_shared_links",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
fontWeight: FontWeight.w500,
fontSize: 12,
),
maxLines: 1,
).tr(),
@@ -236,9 +231,11 @@ class SharingPage extends HookConsumerWidget {
SliverPadding(
padding: const EdgeInsets.all(12),
sliver: SliverToBoxAdapter(
child: const Text(
child: Text(
"partner_page_title",
style: TextStyle(fontWeight: FontWeight.bold),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
).tr(),
),
),
@@ -246,10 +243,10 @@ class SharingPage extends HookConsumerWidget {
SliverPadding(
padding: const EdgeInsets.all(12),
sliver: SliverToBoxAdapter(
child: const Text(
child: Text(
"sharing_page_album",
style: TextStyle(
fontWeight: FontWeight.bold,
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
).tr(),
),

View File

@@ -68,7 +68,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
gravity: ToastGravity.BOTTOM,
);
}
context.pop();
buildContext.pop();
},
);
return const ShareDialog();

View File

@@ -93,15 +93,11 @@ class DescriptionInput extends HookConsumerWidget {
maxLines: null,
keyboardType: TextInputType.multiline,
controller: controller,
style: const TextStyle(
fontSize: 14,
),
style: context.textTheme.labelLarge,
decoration: InputDecoration(
hintText: 'description_input_hint_text'.tr(),
border: InputBorder.none,
hintStyle: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
hintStyle: context.textTheme.labelLarge?.copyWith(
color: textColor.withOpacity(0.5),
),
suffixIcon: suffixIcon,

View File

@@ -193,21 +193,15 @@ class ExifBottomSheet extends HookConsumerWidget {
children: [
Text(
"exif_bottom_sheet_location",
style: TextStyle(
fontSize: 11,
color: textColor,
fontWeight: FontWeight.bold,
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
buildMap(),
RichText(
text: TextSpan(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: textColor,
fontFamily: 'WorkSans',
),
style: context.textTheme.labelLarge,
children: [
if (exifInfo != null && exifInfo.city != null)
TextSpan(
@@ -228,7 +222,9 @@ class ExifBottomSheet extends HookConsumerWidget {
),
Text(
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}",
style: const TextStyle(fontSize: 12),
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(150),
),
),
],
),
@@ -258,10 +254,7 @@ class ExifBottomSheet extends HookConsumerWidget {
titleAlignment: ListTileTitleAlignment.center,
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
style: context.textTheme.labelLarge,
),
subtitle: subtitle,
);
@@ -278,7 +271,7 @@ class ExifBottomSheet extends HookConsumerWidget {
// There is both filename and size information
return createImagePropertiesListStyle(
asset.fileName,
Text(imgSizeString),
Text(imgSizeString, style: context.textTheme.bodySmall),
);
} else if (imgSizeString != null && asset.fileName.isEmpty) {
// There is only size information
@@ -305,10 +298,9 @@ class ExifBottomSheet extends HookConsumerWidget {
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"exif_bottom_sheet_details",
style: TextStyle(
fontSize: 11,
color: textColor,
fontWeight: FontWeight.bold,
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
).tr(),
),
@@ -323,10 +315,7 @@ class ExifBottomSheet extends HookConsumerWidget {
),
title: Text(
"${exifInfo!.make} ${exifInfo.model}",
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
style: context.textTheme.labelLarge,
),
subtitle: exifInfo.f != null ||
exifInfo.exposureSeconds != null ||
@@ -334,6 +323,7 @@ class ExifBottomSheet extends HookConsumerWidget {
exifInfo.iso != null
? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
style: context.textTheme.bodySmall,
)
: null,
),

View File

@@ -28,17 +28,17 @@ class BackupInfoCard extends StatelessWidget {
elevation: 0,
borderOnForeground: false,
child: ListTile(
minVerticalPadding: 15,
minVerticalPadding: 18,
isThreeLine: true,
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
style: context.textTheme.titleMedium,
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8.0),
padding: const EdgeInsets.only(top: 4.0, right: 18.0),
child: Text(
subtitle,
style: const TextStyle(fontSize: 12),
style: context.textTheme.bodyMedium,
),
),
trailing: Column(
@@ -46,9 +46,12 @@ class BackupInfoCard extends StatelessWidget {
children: [
Text(
info,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
style: context.textTheme.titleLarge,
),
const Text("backup_info_card_assets").tr(),
Text(
"backup_info_card_assets",
style: context.textTheme.labelLarge,
).tr(),
],
),
),

View File

@@ -188,9 +188,9 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
Text(
"backup_controller_page_uploading_file_info",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
style: context.textTheme.titleSmall,
).tr(),
if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(),
],

View File

@@ -100,7 +100,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
label: Text(
album.name,
style: TextStyle(
fontSize: 10,
fontSize: 12,
color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
@@ -134,7 +134,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
label: Text(
album.name,
style: TextStyle(
fontSize: 10,
fontSize: 12,
color: isDarkTheme ? Colors.black : immichBackgroundColor,
fontWeight: FontWeight.bold,
),
@@ -203,7 +203,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
),
title: const Text(
"backup_album_selection_page_select_albums",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
).tr(),
elevation: 0,
),
@@ -219,12 +218,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
vertical: 8.0,
horizontal: 16.0,
),
child: const Text(
child: Text(
"backup_album_selection_page_selection_info",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
style: context.textTheme.titleSmall,
).tr(),
),
// Selected Album Chips
@@ -250,19 +246,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
.toString(),
],
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
style: context.textTheme.titleSmall,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
style: TextStyle(
fontSize: 12,
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),

View File

@@ -193,7 +193,7 @@ class BackupControllerPage extends HookConsumerWidget {
: const Icon(Icons.cloud_off_rounded),
title: Text(
backUpOption,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
style: context.textTheme.titleSmall,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
@@ -213,9 +213,8 @@ class BackupControllerPage extends HookConsumerWidget {
.setAutoBackup(!isAutoBackup),
child: Text(
backupBtnText,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
style: context.textTheme.labelLarge?.copyWith(
color: context.isDarkTheme ? Colors.black : Colors.white,
),
),
),
@@ -335,7 +334,7 @@ class BackupControllerPage extends HookConsumerWidget {
isBackgroundEnabled
? "backup_controller_page_background_is_on"
: "backup_controller_page_background_is_off",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
style: context.textTheme.titleSmall,
).tr(),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -426,9 +425,8 @@ class BackupControllerPage extends HookConsumerWidget {
isBackgroundEnabled
? "backup_controller_page_background_turn_off"
: "backup_controller_page_background_turn_on",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
style: context.textTheme.labelLarge?.copyWith(
color: context.isDarkTheme ? Colors.black : Colors.white,
),
).tr(),
),
@@ -511,10 +509,8 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
text.trim().substring(0, text.length - 2),
style: TextStyle(
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
@@ -523,10 +519,8 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
"backup_controller_page_none_selected".tr(),
style: TextStyle(
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
@@ -546,10 +540,8 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Text(
text.trim().substring(0, text.length - 2),
style: TextStyle(
style: context.textTheme.labelLarge?.copyWith(
color: Colors.red[300],
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
@@ -559,55 +551,57 @@ class BackupControllerPage extends HookConsumerWidget {
}
buildFolderSelectionTile() {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: context.isDarkTheme
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: ListTile(
minVerticalPadding: 15,
title: const Text(
"backup_controller_page_albums",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
).tr(),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"backup_controller_page_to_backup",
style: TextStyle(fontSize: 12),
).tr(),
buildSelectedAlbumName(),
buildExcludedAlbumName(),
],
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: context.isDarkTheme
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,
),
),
trailing: ElevatedButton(
onPressed: () async {
await context.autoPush(const BackupAlbumSelectionRoute());
// waited until returning from selection
await ref
.read(backupProvider.notifier)
.backupAlbumSelectionDone();
// waited until backup albums are stored in DB
ref.read(albumProvider.notifier).getDeviceAlbums();
},
child: const Text(
"backup_controller_page_select",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
elevation: 0,
borderOnForeground: false,
child: ListTile(
minVerticalPadding: 18,
title: Text(
"backup_controller_page_albums",
style: context.textTheme.titleMedium,
).tr(),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"backup_controller_page_to_backup",
style: context.textTheme.bodyMedium,
).tr(),
buildSelectedAlbumName(),
buildExcludedAlbumName(),
],
),
),
trailing: ElevatedButton(
onPressed: () async {
await context.autoPush(const BackupAlbumSelectionRoute());
// waited until returning from selection
await ref
.read(backupProvider.notifier)
.backupAlbumSelectionDone();
// waited until backup albums are stored in DB
ref.read(albumProvider.notifier).getDeviceAlbums();
},
child: const Text(
"backup_controller_page_select",
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
),
),
),
);
@@ -657,7 +651,7 @@ class BackupControllerPage extends HookConsumerWidget {
child: const Text(
"backup_controller_page_start_backup",
style: TextStyle(
fontSize: 14,
fontSize: 16,
fontWeight: FontWeight.bold,
),
).tr(),
@@ -680,7 +674,6 @@ class BackupControllerPage extends HookConsumerWidget {
elevation: 0,
title: const Text(
"backup_controller_page_backup",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
).tr(),
leading: IconButton(
onPressed: () {
@@ -735,7 +728,6 @@ class BackupControllerPage extends HookConsumerWidget {
if (showBackupFix) const Divider(),
if (showBackupFix) buildCheckCorruptBackups(),
const Divider(),
const Divider(),
const CurrentUploadingAssetInfoBox(),
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
buildBackupButton(),

View File

@@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/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';
class GroupDividerTitle extends ConsumerWidget {
class GroupDividerTitle extends HookConsumerWidget {
const GroupDividerTitle({
Key? key,
required this.text,
@@ -21,6 +25,18 @@ class GroupDividerTitle extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final groupBy = useState(GroupAssetsBy.day);
useEffect(
() {
groupBy.value = GroupAssetsBy.values[
appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
return null;
},
[],
);
void handleTitleIconClick() {
HapticFeedback.heavyImpact();
if (selected) {
@@ -31,8 +47,8 @@ class GroupDividerTitle extends ConsumerWidget {
}
return Padding(
padding: const EdgeInsets.only(
top: 12.0,
padding: EdgeInsets.only(
top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0,
bottom: 16.0,
left: 12.0,
right: 12.0,
@@ -41,10 +57,14 @@ class GroupDividerTitle extends ConsumerWidget {
children: [
Text(
text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
style: groupBy.value == GroupAssetsBy.month
? context.textTheme.bodyLarge?.copyWith(
fontSize: 24.0,
)
: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withAlpha(250),
fontWeight: FontWeight.w500,
),
),
const Spacer(),
GestureDetector(

View File

@@ -222,10 +222,9 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
child: Text(
title,
style: TextStyle(
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: context.textTheme.displayLarge?.color,
fontWeight: FontWeight.w500,
),
),
);

View File

@@ -358,7 +358,7 @@ class LoginForm extends HookConsumerWidget {
TextButton.icon(
icon: const Icon(Icons.arrow_back),
onPressed: () => serverEndpoint.value = null,
label: const Text('Back'),
label: const Text('login_form_back_button_text').tr(),
),
],
),

View File

@@ -176,10 +176,10 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
Widget buildDragHandle(ScrollController scrollController) {
final textToDisplay = assetsInBound.value.isNotEmpty
? "${assetsInBound.value.length} photo${assetsInBound.value.length > 1 ? "s" : ""}"
? "map_assets_in_bounds".plural(assetsInBound.value.length)
: "map_no_assets_in_bounds".tr();
final dragHandle = Container(
height: 60,
height: 70,
width: double.infinity,
decoration: BoxDecoration(
color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
@@ -195,11 +195,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
const SizedBox(height: 15),
Text(
textToDisplay,
style: TextStyle(
fontSize: 16,
color: context.textTheme.displayLarge?.color,
fontWeight: FontWeight.bold,
),
style: context.textTheme.bodyLarge,
),
Divider(
height: 10,

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