Compare commits

..

108 Commits

Author SHA1 Message Date
Immich Release Bot
d77a1aba7a Version v1.44.0 2023-02-02 07:13:46 +00:00
Alex Tran
9e21b16553 Revert "Version v1.44.1"
This reverts commit ab2c019a7a.
2023-02-02 01:03:01 -06:00
Alex
dcb56ae775 fix(ci) pump script reset minor and patch based on major and minor pump (#1515) 2023-02-02 00:53:22 -06:00
Immich Release Bot
ab2c019a7a Version v1.44.1 2023-02-02 06:43:35 +00:00
Alex
eb408d4858 Inherit secrect in workflow (#1514) 2023-02-02 00:15:27 -06:00
Alex
4f38851880 fix(CI): fix mobile build artifact with proper signing (#1504) 2023-02-01 21:50:07 -06:00
martyfuhry
2c356ec87f fix(mobile) uses clamping scroll physics on android (#1503) 2023-02-01 16:29:32 -06:00
Jason Rasmussen
bb84464216 refactor(server): device info (#1490)
* refactor(server): device info

* fix: export device service

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-02-01 14:55:06 -06:00
Jason Rasmussen
32b9e0bad4 chore: editorconfig (#1505) 2023-02-01 14:38:47 -06:00
martyfuhry
02f5a86ee9 (fix)mobile: Improve the gallery to improve scale, double tap, and swipe gesture detection (#1502)
* photoviewgallery

* stiffer scrolling to react more like google photos

* adds a dx threshhold for the swipe/up down from the original dropped point

* stopped wrapping imageview in gallery viewer to avoid the double photoview issue. breaks imageview page pinch-to-zoom, so i need to fix that for other callers

* refactors gallery view to use remoteimage directly and breaks imageviewpage

* removed image_viewer_page

* adds minscale

* adds photo_view to repository

* double tap to zoom out with hacked commit

* double tapping!

* got up and down swipe gestures working

* fixed wrong cache and headers in image providers

* fixed image quality and added videos back in

* local loading asset image fix

* precaches images

* fixes lint errors

* deleted remote_photo_view and more linters

* fixes scale

* load preview and load original

* precache does original / preview as well

* refactored image providers to nice functions and added JPEG thumbnail format to remote image thumbnail lookup

* moved photo_view to shared/ui/

* three stage loading with webp and fixes some thumbnail fits

* fixed local thumbnail

* fixed paging in iOS
2023-02-01 10:59:34 -06:00
Alex
391bf052e4 Revert "fix(mobile): Generate 1 splash screen on Android (#1443)" (#1498)
This reverts commit 00630bd4a3.
2023-01-31 22:05:54 -06:00
Jason Rasmussen
d2a9363fc5 refactor(server): auth guard (#1472)
* refactor: auth guard

* chore: move auth guard to middleware

* chore: tests

* chore: remove unused code

* fix: migration to uuid without dataloss

* chore: e2e tests

* chore: removed unused guards
2023-01-31 12:11:49 -06:00
dependabot[bot]
68af4cd5ba chore(deps): bump docker/build-push-action from 3.3.0 to 4.0.0 (#1492)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.3.0 to 4.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.3.0...v4.0.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-31 10:01:22 -06:00
dependabot[bot]
c82dcb11e1 chore(deps): bump docker/setup-buildx-action from 2.3.0 to 2.4.0 (#1493)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-31 10:00:20 -06:00
Fynn Petersen-Frey
d0f8d8d1f9 fix(server): re-enable Redis unix socket support (#1494) 2023-01-31 09:59:59 -06:00
bo0tzz
6a852332de feat(build): Include apk in prepare-release flow (#1496) 2023-01-31 09:59:37 -06:00
Alex
830fec0c29 feat(mobile) reused HTTP client for uploading assets (#1487)
* feat(mobile) reused HTTP client for uploading assets

* remove unused comments
2023-01-30 16:00:03 -06:00
dependabot[bot]
aa68d35f42 chore(deps): bump docker/setup-buildx-action from 2.2.1 to 2.3.0 (#1483)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-30 10:15:33 -06:00
dependabot[bot]
6e6fe9bc87 chore(deps): bump actions/upload-artifact from 1 to 3 (#1482)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-30 10:15:18 -06:00
dependabot[bot]
29f68e6dbb chore(deps): bump actions/checkout from 2 to 3 (#1481)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-30 10:14:58 -06:00
Jason Rasmussen
9428b2576b refactor(server): asset service - upload asset (#1438)
* refactor: asset upload

* refactor: background service

* chore: tests

* Regenerate api

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-01-30 10:14:13 -06:00
martyfuhry
3210302ecd fix(mobile): Fix back button closing the app from multiselection in Android (#1477)
* fixes back button multiselection on android in main timeline

* back button on multiselect in album clears selection

* fixed homepage back and refactor future

* not a futureOr
2023-01-29 22:36:26 -06:00
martyfuhry
f23979024a chore(mobile): Update share_file to latest version and migrate to using cross platform shareXFile (#1476)
* update share_plus and use sharexfile

* rename variable
2023-01-29 20:46:30 -06:00
Alex
870a65fa6d fix(server): Cannot remove album with shared links (#1479) 2023-01-29 20:13:34 -06:00
hydazz
199eb20b66 Update unraid.md (#1475) 2023-01-29 09:44:27 -06:00
Alex
91114e5aa0 fix(mobile) add and delete asset in album doesn' (#1474)
t update count in list
2023-01-28 21:57:13 -06:00
Alex
dfbc831525 fix(web) long album name break styling for shared album card (#1473) 2023-01-28 21:10:05 -06:00
Alex
1a640609c7 chore(mobile): Build and sign APK in GitHub Action (#1471)
* chore(mobile): Build and sign APK in GitHUb Action

* fix-1: working directory

* fix-2: working directory

* fix-3: key ALIAS

* fix-4: build apk

* fix-5: naming
2023-01-28 17:24:42 -06:00
martyfuhry
00630bd4a3 fix(mobile): Generate 1 splash screen on Android (#1443) 2023-01-28 16:51:38 -06:00
Skyler Mäntysaari
fb408d7aa3 chore(server): cookie changes to SameSite=Lax (#1467)
* fix(server/cookie): cookie should have SameSite=Lax.

* Forgot to update tests.
2023-01-28 16:33:03 -06:00
martyfuhry
6b5d6e4091 fix(mobile): top padding missing from drag handle in add to album bottom sheet (#1462) 2023-01-28 15:05:28 -06:00
Alex
a287be1f0c chore: Update .gitignore and openapi version (#1461)
* chore: Update gitignore to ignore mobile/openapi/pubspec.lock

* Update openapi version
2023-01-28 09:19:22 -06:00
Jason Rasmussen
42a3149fe3 refactor(server): move asset upload job to domain (#1434)
* refactor: move to domain

* refactor: rename method

* Update comments

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-01-27 23:57:37 -06:00
Kiel Hurley
5aee5c0fb8 feat(web): More localisation (#1441)
* File size localisation

* Localisation for sidebar tooltips

* Localisation for active/waiting jobs

* Localisation for selected item counts

* Prettier

* Ignore Jest coverage directory for Prettier
2023-01-27 23:57:25 -06:00
spiralham
12ecf366b0 chore(mobile): Add adaptive launcher icon for Android (#1457) 2023-01-27 23:18:42 -06:00
Immich Release Bot
275562bce0 Version v1.43.1 2023-01-28 05:13:59 +00:00
Jason Rasmussen
a09030fd6d fix(server): microservices port (#1460) 2023-01-27 23:12:38 -06:00
Jason Rasmussen
414893a687 fix(server): auth strategies (#1459)
* fix(server): auth strategies

* chore: tests
2023-01-27 23:12:11 -06:00
martyfuhry
5939d79057 fix(mobile): removed unused album_list file (#1455) 2023-01-27 21:39:42 -06:00
bo0tzz
189bd37e71 docs: Update links to deployment files (#1405) 2023-01-27 21:26:15 -06:00
Jason Rasmussen
715056047c chore(build): pump ios fastfile (#1450) 2023-01-27 16:20:58 -06:00
Alex
0220f900c1 fix(mobile): Pump ios version and fix static code test (#1451)
* fix(mobile): Pump ios version and fix static code test

* Added changelog note
2023-01-27 16:20:45 -06:00
Immich Release Bot
10a0e58572 Version v1.43.0 2023-01-27 21:06:22 +00:00
martyfuhry
8d47798fa2 feat(mobile): Add multi selected assets to album (#1446)
* refactored to use multiple assets in AddToAlbumList

* add to album from multiselect

* consistent language

* fixed accidental boolean
2023-01-27 15:05:08 -06:00
Zack Pollard
3f2513a717 feat(server): move authentication to tokens stored in the database (#1381)
* chore: add typeorm commands to npm and set default database config values

* feat: move to server side authentication tokens

* fix: websocket should emit error and disconnect on error thrown by the server

* refactor: rename cookie-auth-strategy to user-auth-strategy

* feat: user tokens and API keys now use SHA256 hash for performance improvements

* test: album e2e test remove unneeded module import

* infra: truncate api key table as old keys will no longer work with new hash algorithm

* fix(server): e2e tests (#1435)

* fix: root module paths

* chore: linting

* chore: rename user-auth to strategy.ts and make validate return AuthUserDto

* fix: we should always send HttpOnly for our auth cookies

* chore: remove now unused crypto functions and jwt dependencies

* fix: return the extra fields for AuthUserDto in auth service validate

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-01-27 14:50:07 -06:00
Matthias Rupp
9be71f603e fix(mobile): Fix endless 'Building timeline' loop after changing the number of assets per row (#1445) 2023-01-27 13:08:05 -06:00
bo0tzz
d354b38139 build: Use explicit token in release build (#1444) 2023-01-27 09:41:25 -06:00
bo0tzz
1152cd4f07 docs: Split features into new administration category (#1440)
* docs: Split features into new administration category

* docs: Add redirects for moved pages
2023-01-27 08:32:52 -06:00
James
de0e218440 feat(web): add Favorites page (#1397)
* Duplicate photos page and rename to favorites

* Implement basic functionality to page

* Sort imports

* Add missing sharing code

* Remove unused import

* Fix formatting

* Use GalleryViewer and new api endpoint

* Merge useFavorites into page

* Run prettier

* Move favorites in side-bar

* Remove favorites when unfavorited

* Fix close shared link model

* Add favorite count to side-bar

* Add add to favorites option

* Fix formatting

* Add favorite icon to image thumbnails

* Change var to let
2023-01-27 08:32:26 -06:00
martyfuhry
d377cf0d02 feat(mobile): Add to album from asset detail view (#1413)
* add to album from asset detail view

* layout and design

* added shared albums

* fixed remote, asset update, and hit test

* made static size

* fixed create album

* suppress shared expansion tile if there are no shared albums

* updates album

* padding on tile
2023-01-26 23:16:28 -06:00
Alex
788b435f9b feat(web/server): Add options to rerun job on all assets (#1422) 2023-01-26 22:50:22 -06:00
Jason Rasmussen
6ea91b2dde feat: columns on small screens (#1433) 2023-01-26 20:52:27 -06:00
Jason Rasmussen
55d883925f chore(server): rename database connection variables (#1437) 2023-01-26 20:52:13 -06:00
Jason Rasmussen
89aff7764d chore(server): remove deprecated device endpoints (#1436)
* chore: remove endpoints

* chore: generate open-api
2023-01-26 20:51:22 -06:00
Jason Rasmussen
c4e1bc35b4 feature(server): compute sha1 during upload (#1424)
* feature(server): compute sha1 during upload

* fix: clean up stream on error
2023-01-26 13:29:44 -06:00
bo0tzz
7e53e33e0f build(docker): Use ghcr.io as build cache instead of gha (#1429)
* build(docker): Use github registry as build cache

* build(docker): Only write to cache if not PR
2023-01-26 13:22:46 -06:00
Alex
8b73c2bf8a fix(server): Handle exposure time correctly (#1432) 2023-01-26 13:14:05 -06:00
Matthias Rupp
bcb0056b55 chore(mobile): Run dart analyze in CI (#1425)
* Run dart analyze in CI

* Add pub get

* Fix linter errors in mobile code
2023-01-26 08:40:19 -06:00
Jason Rasmussen
b1311547b2 refactor(cli): use service instead of typeorm repo (#1423) 2023-01-26 08:30:33 -06:00
Alex Tran
bddba4bd96 Merge branch 'main' of github.com:immich-app/immich 2023-01-25 10:35:42 -06:00
Jason Rasmussen
8f304b8157 refactor(server): shared links (#1385)
* refactor(server): shared links

* chore: tests

* fix: bugs and tests

* fix: missed one expired at

* fix: standardize file upload checks

* test: lower flutter version

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-01-25 10:35:28 -06:00
Alex Tran
6acfac9064 Merge branch 'main' of github.com:immich-app/immich 2023-01-25 10:12:01 -06:00
Matthias Rupp
f64db3a2f9 chore(mobile): add login integration tests and reorganize CI definitions (#1417)
* Add integration tests for the login process

* Reorganize tests

* Test wrong instance URL

* Run mobile unit tests in CI

* Fix CI

* Pin Flutter Version to 3.3.10

* Push something stupid to re-trigger CI
2023-01-25 10:10:04 -06:00
Alex
d1db47ee34 chore(docs): Update funding method (#1420)
* Add liberapay to funding option

* Update info

* Update docs
2023-01-25 10:07:58 -06:00
bo0tzz
3b1f27b674 docs(install): Add kubernetes deployment documentation (#1418)
* docs: Make some room in sidebar ordering

* docs(install): Add kubernetes deployment documentation
2023-01-25 10:07:25 -06:00
Alex Tran
0e753b245a Add liberapay to funding option 2023-01-25 09:54:25 -06:00
Alex
8b7d7f1666 fix(mobile): Home page app bar icons don't conform to theme change (#1409)
* fix(mobile): Home page app bar icons don't conform to theme change

* Remove unsued code
2023-01-24 12:23:06 -06:00
Alex
0f1afff4c3 [Localizely] Translations update (#1408) 2023-01-24 11:23:37 -06:00
bo0tzz
50c36068e7 build: Add workflow for creating draft releases (#1402)
* build: Change pump-version script to use flags

* build: Create initial prepare-release workflow

* build: Fix release script path

* build: Rename .env.example to example.env

* docs: propagate example.env rename

* build: Fix pump-version script patch argument

* build: Final tweaks to release scripts
2023-01-24 09:26:58 -06:00
Jason Rasmussen
a6f7fdba4e chore: fix bg classes for dark mode (#1404) 2023-01-24 09:25:48 -06:00
Skyler Mäntysaari
9d337bf4dc feat(server/machine-learning): Configurable port (#1386)
* feat(server/machine-learning): Configurable port

* feat(server/machine-learning): Address PR comments.

* feat(server/machine-learning): Simplify

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-01-23 22:18:35 -06:00
Jason Rasmussen
b7d34079d9 feat(server): search by is favorite (#1400)
* feat(server): search by is favorite

* chore: regenerate api

* fix: boolean transform

* chore: remove console log

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-01-23 22:16:20 -06:00
Jason Rasmussen
eade36ee82 refactor(server): auth service (#1383)
* refactor: auth

* chore: tests

* Remove await on non-async method

* refactor: constants

* chore: remove extra async

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-01-23 22:13:42 -06:00
Jason Rasmussen
443d08381a build: version pump script (#1398)
* build: version pump script

* feat: server pump is optional

* chore: remove unused variable

* chore: examples

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-01-23 21:46:37 -06:00
dependabot[bot]
4e6880e520 chore(deps): bump actions/setup-java from 2 to 3 (#1393)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-01-23 20:34:05 -06:00
Alex
9a300d0286 feat(mobile): show current upload asset (#1399)
* Refactor info box

* Added show thumbnail

* Added loading indicator
2023-01-23 17:10:21 -06:00
bo0tzz
9987e3bcef docs(cli): Clarify CLI-in-docker instructions (#1395)
* docs(cli): Clarify CLI-in-docker instructions

* docs(cli): Add more example commands

* docs(cli): Add port to example command

* docs(cli): Really fix the server port this time
2023-01-23 12:41:30 -06:00
dependabot[bot]
cc749858cb chore(deps): bump subosito/flutter-action from 1 to 2 (#1394)
Bumps [subosito/flutter-action](https://github.com/subosito/flutter-action) from 1 to 2.
- [Release notes](https://github.com/subosito/flutter-action/releases)
- [Commits](https://github.com/subosito/flutter-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: subosito/flutter-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 09:11:22 -06:00
dependabot[bot]
89264b3da4 chore(deps): bump actions/cache from 2 to 3 (#1392)
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 09:11:13 -06:00
Alex Tran
3aab8ccb4a Correctly show current backup asset date when createdAt is in the year of 1970 2023-01-22 22:40:56 -06:00
Alex
171ba84741 fix(mobile) invalid creation time on local asset show 1970 as year (#1391) 2023-01-22 22:33:47 -06:00
Chipwingg
83271bb11e Fixed grammatical mistake (#1390) 2023-01-22 21:21:58 -06:00
Matthias Rupp
ffbc9a28ad Fix mobile integration tests again (#1384) 2023-01-22 08:57:17 +01:00
Alex
a65fea4d64 Actually remove http warning message 2023-01-21 23:29:34 -06:00
Alex Tran
182ee3c0da Fix immich-jwt test 2023-01-21 23:10:29 -06:00
Alex Tran
efe204fef8 Add comma to cookie 2023-01-21 22:59:08 -06:00
Alex Tran
026308acc9 run prettier 2023-01-21 22:53:45 -06:00
Alex Tran
3f60cf5377 Fix incorrect job count for VideoConversion on JobPanel 2023-01-21 22:30:50 -06:00
Alex
b07891089f feat(web/server) Add more options to public shared link (#1348)
* Added migration files

* Added logic for shared album level

* Added permission for EXIF

* Update shared link response dto

* Added condition to show download button

* Create and edit link with new parameter:

* Remove deadcode

* PR feedback

* More refactor

* Move logic of allow original file to service

* Simplify

* Wording
2023-01-21 22:15:16 -06:00
Jason Rasmussen
4cfac47674 refactor(server): job repository (#1382)
* refactor(server): job repository

* refactor: job repository

* chore: generate open-api

* fix: job panel

* Remove incorrect subtitle

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-01-21 22:13:36 -06:00
Matthias Rupp
f4c90426a5 feat(mobile): Add integration tests (#1359) 2023-01-21 21:43:28 -06:00
Zlendy
e5d798581c fix(web): update album information when sliding images (#1378) 2023-01-21 21:26:58 -06:00
hydazz
05b79eb77b Update all-in-one.md (#1379)
@martabal has removed his repo and moved to working on the imagegenius immich repo,
2023-01-21 20:18:53 -06:00
Zack Pollard
4e0fe27de3 feat(server): transcoding improvements (#1370)
* feat: support isEdited flag for SettingSwitch

* feat: add transcodeAll ffmpeg settings for extra transcoding control

* refactor: tidy up and rename current video transcoding code + transcode everything

* feat: better video transcoding with ffprobe

analyses video files to see if they are already in the desired format
allows admin to choose to transcode all videos regardless of the current format

* fix: always serve encoded video if it exists

* feat: change video codec option to a select box, limit options

removed previous video codec config option as it's incompatible with new options
removed mapping for encoder to codec as we now store the codec in the config

* feat: add video conversion job for transcoding previously missed videos

* chore: fix spelling of job messages to pluralise assets

* chore: fix prettier/eslint warnings

* feat: force switch targetAudioCodec default to aac to avoid iOS incompatibility

* chore: lint issues after rebase
2023-01-21 20:09:02 -06:00
Skyler Mäntysaari
8eb82836b9 feat(server): Support webm videos (#1365)
* feat(server): Support webm without transcoding.

Transcoding result doesn't appear to be used by anything expect for quicktime.

* feat(server): Fix the asset uploader for .avi

It needs to be transcoded.

* feat(server): Most browsers doesn't support avi so use mp4.

* feat(server): Address PR comments

* Addressed the PR comments

I moved the function that checks the mimetype to a central location in asset-utils and made tests for it.

* Rollbacked to the way transcoder was decising things to transcode.
2023-01-21 15:52:40 -06:00
Skyler Mäntysaari
5262e92b9f fix(server/cookies): Making the cookie better (#1366)
* fix(server/cookies): Making the cookie better

Cookie should have SameSite=Stict and Secure if served via https, otherwise just SameSite=Strict set.

* feat(server): forgot to add secure to the other cookie.

* Fixed the cookies and tests for them.
2023-01-21 10:16:53 -06:00
Jason Rasmussen
c0a6b3d5a3 refactor(server): system config (#1353)
* refactor(server): system config

* fix: jest circular import

* chore: ignore migrations in coverage report

* chore: tests

* chore: tests

* chore: todo note

* chore: remove vite config backup

* chore: fix redis hostname
2023-01-21 10:11:55 -06:00
Skyler Mäntysaari
66cd7dd809 feat(ci): Update the sdk_update workflow to not run on forks. (#1375) 2023-01-21 10:08:06 -06:00
bo0tzz
c90a88fb17 feat(ci): Consolidate docker build workflow (#1374)
* Consolidate docker build into single workflow

* ci: Only push to altran1502 on release

* ci: Tweaks

* feat(ci): Remove metadata key from permissions

* feat(ci): workaround for buildx regression

* Drop buildkit version to workaround regression

* Revert "Drop buildkit version to workaround regression"

This reverts commit 79adadb2d3.

* Use repo owner name for ghcr login

* feat(ci): Skip docker push on PRs from fork

* feat(ci): Remove explicit permissions config

* temp: Skip docker hub login

* Revert "temp: Skip docker hub login"

This reverts commit e92864d1a3.

* Remove fetch-depth from checkout action
2023-01-21 09:38:27 -06:00
Alex Tran
de4a699c46 Merge branch 'main' of github.com:immich-app/immich 2023-01-20 14:46:35 -06:00
Alex Tran
149d0da724 Remove log file 2023-01-20 14:46:27 -06:00
Hammer
5340683199 Allow the use of SSL connections to the postgres database. (#1256)
* Allow the use of SSL connections to the postgres database.

* Add default SSL false when no env set

* Add commented out example of DB_SSL env

* Refactor add SSL option into PostgresConnectionOptions

* Refactor the database connection to optionally use a URL string instead of the env variables

* Refactor the database connection based on feedback

* Add dynamic validation around the DB envs

* Remove DB_URL from example

* Fix rebase

* Add back the optional database port in the example

* Formatted file correctly

* change types to a const to fix tests
2023-01-20 14:27:01 -06:00
Alex Tran
652f5cbf20 Update readme 2023-01-20 11:34:19 -06:00
bo0tzz
8094b25185 Rebind PUBLIC_* env vars inside web container entrypoint (#1363) 2023-01-20 10:36:13 -06:00
Jason Rasmussen
bdad18a572 feat(server): turn off machine learning endpoint (#1361) 2023-01-20 10:35:55 -06:00
Matthias Rupp
a8cbda5f24 Fix crash at first start when 'userInfoBox' does not contain 'serverEndpointKey' before API service is initialized (#1362) 2023-01-19 13:06:57 -06:00
Alex Tran
753d81adad Fix incorrect way to access the environment variable in Svelte 2023-01-19 10:51:39 -06:00
Connery Noble
43e9529ce4 feat(.well-known): add .well-known/immich to reference API endpoint (#1308)
* feat(.well-known): add .well-known/immich to reference API endpoint

* feat(.well-known): make schema optional (defaults to https)

* adjust method comment to be a little less confusing

* fix casting issue with resovled url

* include when checking Well-known, update server hint

* add validation for login form's server url

* consolidate common process into resolveAndSetEndpoint

* fix missed prettier formatting

* revert translation changes

* update environment variable description, hopefully a bit clearer

* rename environment variable to IMMICH_API_URL_EXTERNAL

* comment out optional env variables

* fix(web): browser-side api client to include authorization token

* Revert "fix(web): browser-side api client to include authorization token"

This reverts commit 60e338938f.

* remove multi-domain related changes
2023-01-19 09:45:37 -06:00
489 changed files with 12682 additions and 8218 deletions

19
.editorconfig Normal file
View File

@@ -0,0 +1,19 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.{ts,js}]
quote_type = single
[*.{md,mdx}]
max_line_length = off
trim_trailing_whitespace = false
[*.{yml,yaml}]
quote_type = double

1
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,5 @@
# These are supported funding model platforms
github: alextran1502
liberapay: alex.tran1502
custom: https://www.buymeacoffee.com/altran1502

53
.github/workflows/build-mobile.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Build Mobile
on:
workflow_dispatch:
workflow_call:
pull_request:
push:
branches: [main]
jobs:
build-sign-android:
name: Build and sign Android
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "12.x"
cache: "gradle"
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.3.10"
cache: true
- name: Create the Keystore
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
run: echo $KEY_JKS | base64 -d > android/key.jks
- name: Get Packages
working-directory: ./mobile
run: flutter pub get
- name: Build Android App Bundle
working-directory: ./mobile
env:
ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
run: flutter build apk --release
- name: Publish Android Artifact
uses: actions/upload-artifact@v3
with:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/app-release.apk

View File

@@ -1,152 +0,0 @@
name: Build and Push Docker Image - Latest
on:
workflow_dispatch:
push:
branches: [main]
jobs:
# This image include both the server and microservices - the two containers can be slitted into separated
# service with its coressponding entry file.
build_and_push_server_monorepo_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.3.0
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
altran1502/immich-server:latest
ghcr.io/${{ github.repository_owner }}/immich-server:latest
build_and_push_machine_learning_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.3.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
altran1502/immich-machine-learning:latest
ghcr.io/${{ github.repository_owner }}/immich-machine-learning:latest
build_and_push_web_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.3.0
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: true
tags: |
altran1502/immich-web:latest
ghcr.io/${{ github.repository_owner }}/immich-web:latest
build_and_push_nginx_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Proxy
uses: docker/build-push-action@v3.3.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true
tags: |
altran1502/immich-proxy:latest
ghcr.io/${{ github.repository_owner }}/immich-proxy:latest

View File

@@ -1,168 +0,0 @@
name: Build and Push Docker Image - Staging
on:
workflow_dispatch:
pull_request:
branches: [main]
jobs:
# This image include both the server and microservices - the two containers can be slitted into separated
# service with its coressponding entry file.
build_and_push_server_monorepo_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.3.0
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
altran1502/immich-server:staging
altran1502/immich-server:${{ github.event.pull_request.number }}
ghcr.io/${{ github.repository_owner }}/immich-server:staging
ghcr.io/${{ github.repository_owner }}/immich-server:${{ github.event.pull_request.number }}
build_and_push_machine_learning_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.3.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
altran1502/immich-machine-learning:staging
altran1502/immich-machine-learning:${{ github.event.pull_request.number }}
ghcr.io/${{ github.repository_owner }}/immich-machine-learning:staging
ghcr.io/${{ github.repository_owner }}/immich-machine-learning:${{ github.event.pull_request.number }}
build_and_push_web_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.3.0
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-web:staging
altran1502/immich-web:${{ github.event.pull_request.number }}
ghcr.io/${{ github.repository_owner }}/immich-web:staging
ghcr.io/${{ github.repository_owner }}/immich-web:${{ github.event.pull_request.number }}
build_and_push_nginx_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Proxy
uses: docker/build-push-action@v3.3.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-proxy:staging
altran1502/immich-proxy:${{ github.event.pull_request.number }}
ghcr.io/${{ github.repository_owner }}/immich-proxy:staging
ghcr.io/${{ github.repository_owner }}/immich-proxy:${{ github.event.pull_request.number }}

View File

@@ -1,197 +0,0 @@
name: Build and push Docker image - Release
on:
workflow_dispatch:
release:
types: [published]
jobs:
build_and_push_server_monorepo_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'main'
fetch-depth: 0
- name: 'Get Previous tag'
id: previoustag
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push immich-server release
uses: docker/build-push-action@v3.3.0
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
altran1502/immich-server:${{ steps.previoustag.outputs.tag }}
altran1502/immich-server:release
ghcr.io/${{ github.repository_owner }}/immich-server:${{ steps.previoustag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/immich-server:release
build_and_push_machine_learning_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 'Get Previous tag'
id: previoustag
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.3.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
altran1502/immich-machine-learning:release
ghcr.io/${{ github.repository_owner }}/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/immich-machine-learning:release
build_and_push_web_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'main'
fetch-depth: 0
- name: 'Get Previous tag'
id: previoustag
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push immich-web release
uses: docker/build-push-action@v3.3.0
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
target: prod
tags: |
altran1502/immich-web:${{ steps.previoustag.outputs.tag }}
altran1502/immich-web:release
ghcr.io/${{ github.repository_owner }}/immich-web:${{ steps.previoustag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/immich-web:release
build_and_push_nginx_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'main'
fetch-depth: 0
- name: 'Get Previous tag'
id: previoustag
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push immich-proxy release
uses: docker/build-push-action@v3.3.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: |
altran1502/immich-proxy:release
altran1502/immich-proxy:${{ steps.previoustag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/immich-proxy:${{ steps.previoustag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/immich-proxy:release

View File

@@ -8,6 +8,7 @@ on:
jobs:
update-sdk-repos:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.head.repo.fork }}
steps:
- uses: actions/github-script@v6
with:

100
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Build and Push Docker Images
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
release:
types: [published]
jobs:
build_and_push:
runs-on: ubuntu-latest
strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false
matrix:
include:
- context: "server"
image: "immich-server"
- context: "web"
image: "immich-web"
- context: "machine-learning"
image: "immich-machine-learning"
- context: "nginx"
image: "immich-proxy"
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.4.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v4
with:
flavor: |
# Disable latest tag
latest=false
images: |
name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Determine build cache output
id: cache-target
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
fi
- name: Build and push image
uses: docker/build-push-action@v4.0.0
with:
context: ${{ matrix.context }}
platforms: linux/arm/v7,linux/amd64,linux/arm64
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
tags: ${{ steps.metadata.outputs.tags }}

62
.github/workflows/prepare-release.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Prepare new release
on:
workflow_dispatch:
inputs:
serverBump:
description: "Bump server version"
required: true
default: "false"
type: choice
options:
- "false"
- minor
- patch
mobileBump:
description: "Bump mobile build number"
required: false
type: boolean
jobs:
build_mobile:
uses: ./.github/workflows/build-mobile.yml
secrets: inherit
tag_release:
runs-on: ubuntu-latest
needs: build_mobile
steps:
- name: Checkout
uses: actions/checkout@v3
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
- name: Bump version
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
- name: Commit and tag
uses: EndBug/add-and-commit@v9
with:
author_name: Immich Release Bot
author_email: bot@immich.app
message: "Version ${{ env.IMMICH_VERSION }}"
tag: ${{ env.IMMICH_VERSION }}
push: true
- name: Download APK
uses: actions/download-artifact@v3
with:
name: release-apk-signed
- name: Create draft release
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ env.IMMICH_VERSION }}
generate_release_notes: true
body_path: misc/release/notes.tmpl
files: |
docker/docker-compose.yml
docker/example.env
*.apk

31
.github/workflows/static_analysis.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Static Code Analysis
on:
workflow_dispatch:
pull_request:
push:
branches: [main]
jobs:
mobile-dart-analyze:
name: Run Dart Code Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
- name: Install dependencies
run: dart pub get
working-directory: ./mobile
- name: Run dart analyze
run: dart analyze --fatal-infos
working-directory: ./mobile

View File

@@ -39,3 +39,56 @@ jobs:
- name: Run tests
run: cd web && npm ci && npm run check:all
mobile-unit-tests:
name: Run mobile unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
- name: Run tests
working-directory: ./mobile
run: flutter test
mobile-integration-tests:
name: Run mobile end-to-end integration tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- name: Cache android SDK
uses: actions/cache@v3
id: android-sdk
with:
key: android-sdk
path: |
/usr/local/lib/android/
~/.android
- name: Setup Android SDK
if: steps.android-sdk.outputs.cache-hit != 'true'
uses: android-actions/setup-android@v2
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2.27.0
with:
working-directory: ./mobile
api-level: 29
arch: x86_64
profile: pixel
target: default
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
disable-linux-hw-accel: false
script: |
flutter pub get
flutter test integration_test

5
.gitignore vendored
View File

@@ -4,4 +4,9 @@
.idea
docker/upload
uploads
coverage
mobile/gradle.properties
mobile/openapi/pubspec.lock
mobile/*.jks

View File

@@ -79,6 +79,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| OAuth support | Yes | Yes |
| LivePhoto backup and playback | iOS | Yes |
| User-defined storage structure | Yes | Yes |
| Public Sharing | N/A | Yes |
# Support the project
@@ -92,6 +93,9 @@ If you feel like this is the right cause and the app is something you are seeing
- [Monthly donation](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [One-time donation](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via Github Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
# Known Issues

View File

@@ -5,14 +5,11 @@ DB_PASSWORD=postgres
DB_DATABASE_NAME=e2e_test
# Redis
REDIS_HOSTNAME=immich_redis_test
REDIS_HOSTNAME=immich-redis-test
# Upload File Config
UPLOAD_LOCATION=./upload
# JWT SECRET
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
# MAPBOX
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false

View File

@@ -1,4 +1,4 @@
version: '3.8'
version: "3.8"
services:
immich-server:
@@ -14,6 +14,7 @@ services:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
ports:
- 3001:3001
- 9230:9230
env_file:
- .env
@@ -75,6 +76,7 @@ services:
environment:
# Rename these values for svelte public interface
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
ports:
- 3000:3000
- 24678:24678

View File

@@ -54,6 +54,7 @@ services:
environment:
# Rename these values for svelte public interface
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
restart: always
redis:

View File

@@ -51,9 +51,6 @@ services:
entrypoint: ["/bin/sh", "./entrypoint.sh"]
env_file:
- .env
environment:
# Rename these values for svelte public interface
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
restart: always
redis:

View File

@@ -30,16 +30,6 @@ REDIS_HOSTNAME=immich_redis
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
###################################################################################
# JWT SECRET
#
# This JWT_SECRET is used to sign the authentication keys for user login
# You should set it to a long randomly generated value
# You can use this command to generate one: openssl rand -base64 128
###################################################################################
JWT_SECRET=
###################################################################################
# Reverse Geocoding
#
@@ -76,3 +66,14 @@ PUBLIC_LOGIN_PAGE_MESSAGE=
IMMICH_WEB_URL=http://immich-web:3000
IMMICH_SERVER_URL=http://immich-server:3001
IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
####################################################################################
# Alternative API's External Address - Optional
#
# This is an advanced feature used to control the public server endpoint returned to clients during Well-known discovery.
# You should only use this if you want mobile apps to access the immich API over a custom URL. Do not include trailing slash.
# NOTE: At this time, the web app will not be affected by this setting and will continue to use the relative path: /api
# Examples: http://localhost:3001, http://immich-api.example.com, etc
####################################################################################
#IMMICH_API_URL_EXTERNAL=http://localhost:3001

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---
# FAQ
@@ -20,9 +20,9 @@ Immich doesn't have the mechanism to sync an existing directory with the server.
The initial approach of Immich is to become a backup tool, primarily for mobile device usage. Thus, all the assets must be uploaded from the mobile client. The app was architectured to perform that job well.
### What happens to existing files after I choose a new [Storage Template](/docs/features/storage-template.mdx)?
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/features/jobs.md) page.
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs.md) page.
### Why is object detection not very good?
@@ -38,11 +38,11 @@ Most Immich components are typically deployed using docker. To see logs for depl
2. Set the corresponding `user` argument in `docker-compose` for each service.
3. Add an additional volume to `immich-microservices` that mounts internally to `/usr/src/app/.reverse-geocoding-dump`.
The non-root user/group needs will need read/write access to the volume mounts, including `UPLOAD_LOCATION`.
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
### How can I reset the admin password?
The admin password can be reset by running the [reset-admin-password](/docs/features/server-commands.md) command on the immich-server.
The admin password can be reset by running the [reset-admin-password](/docs/administration/server-commands.md) command on the immich-server.
### How can I **purge** data from Immich?

View File

@@ -0,0 +1,5 @@
{
"label": "Administration",
"position": 4
}

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -18,6 +18,6 @@ Several Immich functionalities are implemented as jobs, which run in the backgro
## Storage Migration
This job can be run after changing the [Storage Template](/docs/features/storage-template.mdx), in order to apply the change to the existing library.
This job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
![Storage Migration](./img/admin-jobs-template.png)

View File

@@ -14,19 +14,19 @@ To toggle the password login setting via the web, navigate to the "Administratio
### Server Command
There are two [Server Commands](/docs/features/server-commands.md) for password login:
There are two [Server Commands](/docs/administration/server-commands.md) for password login:
1. `enable-password-login`
2. `disable-password-login`
See [Server Commands](/docs/features/server-commands.md) for more details about how to run them.
See [Server Commands](/docs/administration/server-commands.md) for more details about how to run them.
## Password Reset
### Admin
To reset the administrator password, use the `reset-admin-password` [Server Command](/docs/features/server-commands.md).
To reset the administrator password, use the `reset-admin-password` [Server Command](/docs/administration/server-commands.md).
### User
Immich does not currently support self-service password reset. However, the administration can reset passwords for other users. See [User Management: Password Reset](/docs/features/user-management.mdx#password-reset) for more information about how to do this.
Immich does not currently support self-service password reset. However, the administration can reset passwords for other users. See [User Management: Password Reset](/docs/administration/user-management.mdx#password-reset) for more information about how to do this.

View File

@@ -1,4 +1,4 @@
{
"label": "Developer",
"position": 4
"position": 5
}

View File

@@ -23,8 +23,8 @@ All the services are packaged to run as with single Docker Compose command.
### Instructions
1. Clone the project repo.
2. Run `cp docker/.env.example docker/.env`.
3. Edit `docker/.env` to provide values for the required variables `UPLOAD_LOCATION` and `JWT_SECRET`.
2. Run `cp docker/example.env docker/.env`.
3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
4. From the root directory, run:
```bash title="Start development server"
@@ -99,7 +99,7 @@ After making any changes in the `server/libs/database/src/entities`, a database
2. Run
```bash
npm run typeorm -- migration:generate ./libs/database/src/<migration-name> -d libs/database/src/config/database.config.ts
npm run typeorm -- migration:generate ./libs/infra/src/db/<migration-name> -d ./libs/infra/src/db/config/database.config.ts
```
3. Check if the migration file makes sense.

View File

@@ -46,19 +46,37 @@ The API key can be obtained in the user setting panel on the web interface.
### Run via Docker
Be aware that as this runs inside a container it mounts your current directory as a volume, and for the -d flag you need to use the path inside the container.
You can run the CLI inside of a docker container to avoid needing to install anything.
```bash
docker run -it --rm -v $(pwd):/import ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api -d /import
:::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 --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
Optionally, you can create an alias:
```bash title="Upload target directory"
docker run -it --rm -v /DIRECTORY/WITH/IMAGES:/import ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
```bash
```bash title="Create an alias"
alias immich="docker run -it --rm -v $(pwd):/import ghcr.io/immich-app/immich-cli:latest"
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api -d /import
immich upload --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 --key HFEJ38DNSDUEG --server http://immich-server:3001/
```
:::
### Run from source
```bash title="Clone Repository"

View File

@@ -15,9 +15,9 @@ Users can change their own passwords.
![Change Password](./img/user-change-password.png)
:::tip Reset Password
The admin can reset a password through the [User Management](/docs/features/user-management.mdx) screen.
The admin can reset a password through the [User Management](/docs/administration/user-management.mdx) screen.
:::
:::tip Reset Admin Password
The admin password can be reset using a [Server Command](/docs/features/server-commands.md)
The admin password can be reset using a [Server Command](/docs/administration/server-commands.md)
:::

View File

@@ -1,4 +1,4 @@
{
"label": "Guides",
"position": 5
"position": 6
}

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 99
sidebar_position: 70
---
# All-In-One [Community]
@@ -18,4 +18,4 @@ For installation instructions, refer to the [Github Repository][github].
For issues, open an issue on the associated [GitHub Repository][github].
[github]: https://github.com/martabal/docker-immich
[github]: https://github.com/imagegenius/docker-immich/

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 30
---
# Docker Compose [Recommended]
@@ -8,16 +8,16 @@ Docker Compose is the recommended method to run Immich in production. Below are
### Step 1 - Download the required files
Download [`docker-compose.yml`][compose-file] [`.env.example`][env-file].
Download [`docker-compose.yml`][compose-file] [`example.env`][env-file].
From a directory of your choice (e.g. `./immich-app`) run the following commands:
```bash title="Get docker-compose.yml file"
wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml
wget https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
```
```bash title="Get .env file"
wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
```
### Step 2 - Populate the .env file with custom values
@@ -63,15 +63,6 @@ UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_ba
LOG_LEVEL=simple
###################################################################################
# JWT SECRET
###################################################################################
# This JWT_SECRET is used to sign the authentication keys for user login
# You should set it to a long randomly generated value
# You can use this command to generate one: openssl rand -base64 128
JWT_SECRET=
###################################################################################
# Reverse Geocoding
####################################################################################
@@ -102,11 +93,6 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
- Populate custom database information if necessary.
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
- Populate a secret value for `JWT_SECRET`. You can use the command below to generate a secure key:
```bash title="Command to generate secure JWT_SECRET key"
openssl rand -base64 128
```
### Step 3 - Start the containers
@@ -130,6 +116,6 @@ docker-compose pull && docker-compose up -d # Or `docker compose`
Immich is currently under heavy development, which means you can expect breaking changes and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
:::
[compose-file]: https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml
[env-file]: https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
[watchtower]: https://containrrr.dev/watchtower/

View File

@@ -0,0 +1,24 @@
---
sidebar_position: 40
---
# Kubernetes
You can deploy Immich on Kubernetes using [the official Helm chart](https://github.com/immich-app/immich-charts/tree/main/charts/apps/immich).
If you want examples of how other people run Immich on Kubernetes, using the official chart or otherwise, you can find them at https://nanne.dev/k8s-at-home-search/#/immich.
:::caution DNS in Alpine containers
Immich makes use of Alpine container images. These can encounter [a DNS resolution bug](https://stackoverflow.com/a/65593511) on Kubernetes clusters if the host
nodes have a search domain set, like:
```
$ cat /etc/resolv.conf
search home.lan
nameserver 192.168.1.1
```
When you encounter this bug, it will cause the immich-microservices to crash on startup because it cannot download
the geocoder data. This can be solved in one of two ways: Either reconfigure your nodes to remove the searchdomain from
`resolv.conf`, or set the `DISABLE_REVERSE_GEOCODING` environment variable for Immich to `true` to disable the geocoder.
:::

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 50
---
# Portainer
@@ -9,7 +9,7 @@ Install Immich using Portainer's Stack feature.
1. Go to "**Stacks**" in the left sidebar.
2. Click on "**Add stack**".
3. Give the stack a name (i.e. Immich), and select "**Web Editor**" as the build method.
4. Copy the content of the `docker-compose.yml` file from the [GitHub repository](https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml).
4. Copy the content of the `docker-compose.yml` file from the [GitHub repository](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml).
5. Replace `.env` with `stack.env` for all containers that need to use environment variables in the web editor.
<img
@@ -28,7 +28,7 @@ Install Immich using Portainer's Stack feature.
alt="Dot Env Example"
/>
9. Copy the content of the `.env.example` file from the [GitHub repository](https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example) and paste into the editor.
9. Copy the content of the `example.env` file from the [GitHub repository](https://github.com/immich-app/immich/releases/latest/download/example.env) and paste into the editor.
10. Switch back to "**Simple Mode**".
<img
@@ -40,11 +40,6 @@ Install Immich using Portainer's Stack feature.
* Populate custom database information if necessary.
* Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
* Populate a secret value for `JWT_SECRET`. You can use the command below to generate a secure key:
```bash title="Generate secure JWT_SECRET key"
openssl rand -base64 128
```
11. Click on "**Deploy the stack**".

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 10
---

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 20
---
# Install Script [Experimental]
@@ -16,7 +16,7 @@ curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | b
The script will perform the following actions:
1. Download [docker-compose.yml](https://github.com/immich-app/immich/blob/main/docker/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/blob/main/docker/.env.example) file from the main branch of the [repository](https://github.com/immich-app/immich).
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
2. Populate the `.env` file with necessary information based on the current directory path.
3. Start the containers.

View File

@@ -1,10 +1,28 @@
---
sidebar_position: 5
sidebar_position: 60
---
# Unraid
Immich can easily be installed and updated on Unraid using the [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps.
Immich can easily be installed and updated on Unraid via:
1. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps
2. Community made template on the Unraid Community Apps
## Community Applications Template
:::info
- The Unraid template uses a community made image and is not officially supported by Immich
:::
In order to install Immich from the Unraid CA, you will need an existing Redis and PostgreSQL 14 container, If you do not already have Redis or PostgreSQL you can install them from the Unraid CA, just make sure you choose PostgreSQL **14**.
Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, Choose either of the templates listed and fill out the example variables.
For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup)
## Docker-Compose Method (Official)
:::info
@@ -27,7 +45,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/>
3. Select the cog ⚙️ next to Immich then click "**Edit Stack**"
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml) file into the Unraid editor
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor
<details >
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 5.</summary>
<ul>
@@ -53,9 +71,8 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
</details>
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Past the entire contents of the [Immich .env.example](https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example) file into the Unraid editor, then **before saving** edit the following:
7. Past the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
- `JWT_SECRET`: Generate a unique secret and paste the value here > Can be generated by either typing `openssl rand -base64 128` in your terminal or copying from [uuidgenerator](https://www.uuidgenerator.net/version1)
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
<img

View File

@@ -14,6 +14,10 @@ If you feel like this is the right cause and the app is something you see yourse
- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502)
- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
## Contributing

View File

@@ -15,7 +15,7 @@ function HomepageHeader() {
<p>ON MOBILE DEVICE</p>
</div>
<div className="flex place-items-center place-content-center mt-9 mb-16 gap-4 ">
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 mb-16 gap-4 ">
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold"
to="docs/overview/introduction"

View File

@@ -13,9 +13,15 @@
{ "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/oauth", "destination": "/docs/features/oauth" },
{ "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" },
{ "source": "/docs/usage/server-commands", "destination": "/docs/features/server-commands" }
{ "source": "/docs/usage/server-commands", "destination": "/docs/administration/server-commands" },
{ "source": "/docs/features/jobs", "destination": "/docs/administration/jobs" },
{ "source": "/docs/features/oauth", "destination": "/docs/administration/oauth" },
{ "source": "/docs/features/password-login", "destination": "/docs/administration/password-login" },
{ "source": "/docs/features/server-commands", "destination": "/docs/administration/server-commands" },
{ "source": "/docs/features/storage-template", "destination": "/docs/administration/storage-template" },
{ "source": "/docs/features/user-management", "destination": "/docs/administration/user-management" }
]
}

View File

@@ -28,7 +28,7 @@ download_docker_compose_file() {
download_dot_env_file() {
echo "Downloading .env file..."
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./.env >/dev/null 2>&1
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/example.env -o ./.env >/dev/null 2>&1
}
replace_env_value() {
@@ -45,12 +45,6 @@ populate_upload_location() {
replace_env_value "UPLOAD_LOCATION" $upload_location
}
generate_jwt_secret() {
echo "Generating JWT_SECRET value..."
jwt_secret=$(openssl rand -base64 128)
replace_env_value "JWT_SECRET" $jwt_secret
}
start_docker_compose() {
echo "Starting Immich's docker containers"
@@ -92,5 +86,4 @@ create_immich_directory
download_docker_compose_file
download_dot_env_file
populate_upload_location
generate_jwt_secret
start_docker_compose

View File

@@ -5,7 +5,9 @@ import { Logger } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3003, () => {
const port = Number(process.env.MACHINE_LEARNING_PORT) || 3003;
await app.listen(port, () => {
if (process.env.NODE_ENV == 'development') {
Logger.log(
'Running Immich Machine Learning in DEVELOPMENT environment',

21
misc/release/notes.tmpl Normal file
View File

@@ -0,0 +1,21 @@
## Highlights
{{RELEASE HIGHLIGHTS}}
As always, please consider supporting the project.
🎉 Cheer! 🎉
## Support
<p align="center">
<img src="https://media.giphy.com/media/LStqgGESXW8XnuCv5y/giphy.gif" width="250" title="Loading ~4000 images/videos">
</p>
If you find the project helpful and it helps you in some ways, you can support the project [one time](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or [monthly](https://github.com/sponsors/alextran1502) from GitHub Sponsors
It is a great way to let me know that you want me to continue developing and working on this project for years to come.
## What's Changed

80
misc/release/pump-version.sh Executable file
View File

@@ -0,0 +1,80 @@
#/bin/bash
#
# Pump one or both of the server/mobile versions in appropriate files
#
# usage: './scripts/pump-version.sh -s <major|minor|patch> <-m>
#
# examples:
# ./scripts/pump-version.sh -s major # 1.0.0+50 => 2.0.0+50
# ./scripts/pump-version.sh -s minor -m # 1.0.0+50 => 1.1.0+51
# ./scripts/pump-version.sh -m # 1.0.0+50 => 1.0.0+51
#
SERVER_PUMP="false"
MOBILE_PUMP="false"
while getopts 's:m:' flag; do
case "${flag}" in
s) SERVER_PUMP=${OPTARG} ;;
m) MOBILE_PUMP=${OPTARG} ;;
*)
echo "Invalid args"
exit 1
;;
esac
done
CURRENT_SERVER=$(cat server/package.json | jq -r '.version')
MAJOR=$(echo $CURRENT_SERVER | cut -d '.' -f1)
MINOR=$(echo $CURRENT_SERVER | cut -d '.' -f2)
PATCH=$(echo $CURRENT_SERVER | cut -d '.' -f3)
if [[ $SERVER_PUMP == "major" ]]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif [[ $SERVER_PUMP == "minor" ]]; then
MINOR=$((MINOR + 1))
PATCH=0
elif [[ $SERVER_PUMP == "patch" ]]; then
PATCH=$((PATCH + 1))
elif [[ $SERVER_PUMP == "false" ]]; then
echo 'Skipping Server Pump'
else
echo 'Expected <major|minor|patch|false> for the server argument'
exit 1
fi
NEXT_SERVER=$MAJOR.$MINOR.$PATCH
CURRENT_MOBILE=$(cat mobile/pubspec.yaml | grep "^version: .*+[0-9]\+$" | cut -d "+" -f2)
NEXT_MOBILE=$CURRENT_MOBILE
if [[ $MOBILE_PUMP == "true" ]]; then
set $((NEXT_MOBILE++))
elif [[ $MOBILE_PUMP == "false" ]]; then
echo 'Skipping Mobile Pump'
else
echo "Fatal: MOBILE_PUMP value $MOBILE_PUMP is invalid"
exit 1
fi
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package.json
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package-lock.json
sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/" mobile/ios/fastlane/Fastfile
fi
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
echo "Pumping Mobile: $CURRENT_MOBILE => $NEXT_MOBILE"
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
fi
echo "IMMICH_VERSION=v$NEXT_SERVER" >>$GITHUB_ENV

View File

@@ -59,17 +59,19 @@ android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
def keyAliasVal = System.getenv("ALIAS")
def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD")
def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD")
keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias']
keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword']
storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile'])
storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword']
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
}
}

View File

@@ -0,0 +1,70 @@
<vector android:height="200dp"
android:viewportHeight="1300"
android:viewportWidth="1300"
android:width="200dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#4081ef"
android:fillType="nonZero"
android:pathData="M578,922.6c-2.2,-0.2 -5.5,-1 -9.7,-2.2c-52.4,-15.7 -99,-46.5 -133.8,-88.5c-8.8,-10.7 -17.2,-22.4 -19.4,-27.5c-8.1,-18.1 -6.3,-38.7 4.8,-55.4c5,-7.5 13.2,-15 20.5,-18.7c1.2,-0.6 54.1,-20 55.8,-20.4c0.5,-0.1 0.5,0.2 -0.3,2.1c-0.7,1.7 -1,3.1 -1.1,5.5l-0.1,3.2l2.8,5.8c8.7,17.9 19.2,32.7 33.2,46.4c6.3,6.2 7.8,7.6 13.8,12.3c22.7,18.1 52,30.7 79.9,34.3c2.5,0.3 5,0.8 5.7,1c2.8,0.9 7.7,-0.8 11,-3.7l1.8,-1.6l-0.2,4.8c-0.1,2.7 -0.6,15.4 -1,28.3c-0.6,20.3 -0.8,24 -1.5,27.5c-3.9,20.7 -18.6,37.5 -38.4,44.1c-4.6,1.5 -8,2.2 -13.1,2.7c-4.6,0.5 -5.9,0.4 -10.7,0Z" />
<path
android:fillColor="#31a452"
android:fillType="nonZero"
android:pathData="M707.3,922.4c-4,-0.4 -9.4,-1.6 -13.2,-2.9c-3.4,-1.2 -10,-4.4 -12.5,-6.1c-10.9,-7.4 -19,-17.9 -23.1,-30c-2.2,-6.7 -2.3,-7.5 -3.3,-36.9c-0.5,-14.9 -0.9,-27.9 -0.9,-28.9l-0,-1.9l2.3,1.8c2.6,2 6.6,3.4 8.5,3.1c0.6,-0.1 3,-0.5 5.3,-0.8c37.7,-5.3 71.2,-22.2 97.4,-49.1c12.2,-12.5 21.4,-25.5 29.9,-42.4l3.5,-7l0,-3.6c0,-3.1 -0.1,-3.8 -1,-5.7c-0.5,-1.2 -0.9,-2.1 -0.9,-2.2c0.2,-0.2 55.3,20.1 56.9,20.9c2.6,1.3 6.6,4.1 9.9,7c9.2,7.7 16.1,19.4 18.8,31.8c0.7,3.1 0.8,4.8 0.8,11.3c0,8.6 -0.5,11.7 -2.9,18.7c-1.7,5 -2.9,7.2 -7.1,13.1c-7.6,11 -15.3,20.5 -25.2,31.2c-32.8,35.4 -76.5,62.5 -123.4,76.3c-8,2.5 -12.4,3 -19.8,2.3Z" />
<path
android:fillColor="#de7fb3"
android:fillType="nonZero"
android:pathData="M623.1,811c-25.9,-4.2 -50.7,-14.9 -71.7,-31c-5.2,-4 -8.7,-7.1 -14.1,-12.4c-12.7,-12.5 -21.9,-24.9 -30.5,-41.4c-2.3,-4.4 -2.4,-4.7 -2.4,-7.1c0,-8.8 8.5,-15.2 16.9,-12.7c5.6,1.7 9.6,6.8 9.7,12.2c0,2.6 -0.8,4.6 -2.6,6.2c-1.2,1.1 -3.2,1.9 -4.6,1.9c-1.2,0 -3.3,-0.8 -4.3,-1.6c-2.1,-1.8 -2,-1 0.4,3.2c19.3,33.8 52.3,59.1 90,69.1c5.7,1.5 11.5,2.7 11.8,2.4c0.1,-0.1 -0.4,-0.8 -1.3,-1.6c-5.1,-4.5 -2.3,-11.7 5,-12.8c5.4,-0.8 11.4,2.7 13.9,8c0.8,1.7 1,2.5 1,5.3c0,2.8 -0.1,3.5 -1,5.3c-2,4.3 -6.8,7.9 -10.3,7.8c-0.9,-0.1 -3.6,-0.5 -5.9,-0.8Z" />
<path
android:fillColor="#4081ef"
android:fillType="nonZero"
android:pathData="M665.1,811.2c-3.4,-1.3 -6.4,-4.3 -7.8,-8.1c-1.1,-2.9 -0.9,-7.3 0.5,-10.2c2.6,-5.3 8.7,-8.5 14.4,-7.5c2.9,0.5 4.7,1.9 6,4.3c0.8,1.6 1,2.2 0.8,3.6c-0.3,2.2 -0.9,3.3 -2.7,4.8c-0.8,0.7 -1.4,1.4 -1.3,1.5c0.5,0.5 13.4,-2.7 21.3,-5.4c33.6,-11.3 62.5,-35.1 80.4,-66.1c2.5,-4.4 2.6,-5 0.5,-3.2c-2.8,2.4 -7,1.9 -9.6,-1c-4,-4.6 -0.7,-13.8 6.1,-16.9c2,-0.9 2.7,-1 5.5,-1c2.9,0 3.5,0.1 5.6,1.1c4.4,2.1 7.4,6.4 7.8,11c0.2,2.2 0.1,2.3 -2.2,6.9c-23,45.9 -67,78.1 -117.2,85.9c-5.5,0.9 -6.3,1 -8.1,0.3Z" />
<path
android:fillColor="#31a452"
android:fillType="nonZero"
android:pathData="M578.6,771.5c-4.7,-0.9 -8.7,-2.7 -12.9,-5.9c-10.8,-8.1 -13.5,-22.3 -6.6,-33.7c0.7,-1.2 1.1,-2.2 1,-2.4c-0.2,-0.2 -1.2,-0.6 -2.3,-1.1c-7.6,-3 -13,-10.6 -13.5,-19.1c-0.5,-7.4 3.1,-15 9,-19.4c1,-0.7 2.2,-1.5 2.6,-1.8c0.8,-0.4 68.9,-22.7 69.4,-22.7c0.2,0 0.7,0.7 1.2,1.5c0.5,0.8 1.6,2.3 2.4,3.3c1.2,1.4 1.5,1.9 1.2,2.3c-0.2,0.3 -6.9,9.5 -14.8,20.5c-15.9,21.9 -15.5,21.3 -13.4,23.4c1.3,1.3 2.9,1.4 4.4,0.3c0.6,-0.4 7.5,-9.7 15.5,-20.7c11.2,-15.4 14.6,-19.9 15,-19.7c0.9,0.4 5.5,1.9 6.6,2.1l1,0.2l-0,35.3c-0,39.7 -0,38.8 -2.5,44c-2.6,5.3 -7.2,9.3 -12.7,11.2c-3.7,1.3 -6.8,1.6 -10.2,1c-5.5,-0.9 -9.8,-3.2 -13.7,-7.4l-2.2,-2.4l-0.6,0.9c-3,4.3 -8.6,8.1 -14,9.5c-2.8,0.9 -7.8,1.2 -9.9,0.8Z" />
<path
android:fillColor="#ffb800"
android:fillType="nonZero"
android:pathData="M710.4,771.5c-5.5,-0.9 -9.9,-3.2 -14.3,-7.6l-3.2,-3.2l-0.7,1c-2.3,3.3 -6.8,6.5 -11.1,7.9c-3.7,1.2 -9.2,1.4 -12.6,0.3c-7.1,-2.1 -12.7,-7.4 -15.2,-14.3l-0.9,-2.6l0,-74.2l1.8,-0.4c1,-0.2 2.7,-0.8 3.9,-1.2c1.1,-0.5 2.1,-0.8 2.2,-0.7c0.1,0.1 6.5,9 14.4,19.9c7.8,10.9 14.7,20.1 15.2,20.5c2.2,1.9 5.4,0.4 5.4,-2.6c0,-1.4 -1,-2.9 -13.8,-20.5c-7.6,-10.5 -14.2,-19.6 -14.7,-20.4l-0.9,-1.3l1.4,-1.7c0.8,-0.9 1.9,-2.5 2.5,-3.4l1,-1.6l34.4,11.2c18.9,6.2 35.1,11.6 35.9,12.1c6.8,4 11.1,11.3 11.1,19.1c0,4.1 -0.5,6.4 -2.4,10.2c-2,4.1 -5.5,7.6 -9.6,9.7c-1.6,0.8 -3.2,1.5 -3.4,1.5c-1,0 -0.9,0.7 0.3,2.6c2.8,4.3 4,8.5 3.9,13.7c0,8.1 -3.7,15.2 -10.6,20.3c-6.5,4.8 -13.4,6.7 -20,5.7Z" />
<path
android:fillColor="#de7fb3"
android:fillType="nonZero"
android:pathData="M421.4,714.9c-0.5,-0.1 -2.3,-0.4 -3.9,-0.7c-15.6,-2.6 -30.4,-12.6 -38.8,-26.2c-3.5,-5.7 -6.4,-13.2 -7.8,-19.9c-1.2,-6.1 -0.8,-28.1 0.8,-43.1c4.5,-43 19,-84.3 42.2,-120.7c6.5,-10.2 14.9,-21.5 18.2,-24.6c17.8,-16.6 43.1,-20.5 64.8,-10c4.3,2.1 8.8,5.1 12.7,8.6c2.8,2.4 5.8,6.1 20.9,25.5c9.7,12.5 17.8,22.8 17.9,23c0.2,0.2 -0.9,0.4 -3.2,0.4c-2.5,0 -4.1,0.2 -5.7,0.7c-2.1,0.7 -2.6,1.1 -7.9,6.3c-8.2,8.1 -14.4,15.3 -20.3,23.9c-15.5,22.2 -25.4,47.7 -28.8,74.8c-2.2,16.9 -1.6,37.5 1.6,52.3c0.3,1.4 0.5,2.8 0.4,3c-0.1,0.2 0.2,1.3 0.8,2.4c1.1,2.4 4.3,5.7 6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2 -13.1,3.8 -27.6,8c-16.4,4.7 -27.7,7.8 -29.8,8.1c-3.1,0.4 -11.1,0.6 -13.3,0.2Z" />
<path
android:fillColor="#ffb800"
android:fillType="nonZero"
android:pathData="M862.2,714.7c-2.1,-0.3 -33.8,-9.1 -56.5,-15.8l-2.5,-0.7l1.6,-0.8c3.4,-1.7 7.2,-6.6 7.3,-9.6c0,-0.7 0.4,-3.3 0.8,-5.8c3.9,-22.7 3.1,-46.1 -2.5,-68.4c-6.4,-25.5 -18.6,-49.2 -35.8,-69.1c-4.6,-5.3 -14.8,-15.4 -16.4,-16.1c-2.4,-1.1 -5.1,-1.6 -8,-1.4l-2.7,0.2l1.2,-1.5c0.7,-0.8 8.5,-10.8 17.5,-22.3c8.9,-11.5 17.2,-21.8 18.5,-23.1c2.6,-2.7 7,-6.2 10.3,-8.2c19.3,-11.6 43,-11.1 61.6,1.2c5.4,3.6 8.2,6.2 12.3,11.7c26.4,34.5 44,73.7 52.3,116.2c3.4,17.6 4.9,33.3 5,52.4c0,13 -0.2,14.8 -2.5,21.8c-8.4,26.2 -34.5,42.8 -61.5,39.3Z" />
<path
android:fillColor="#e64132"
android:fillType="nonZero"
android:pathData="M501.4,691.5c-2,-0.5 -4.6,-1.9 -6,-3.3c-2.5,-2.4 -3.1,-3.5 -3.7,-7.3c-4.4,-27.3 -2.2,-54 6.7,-79.3c5.3,-15.1 13.5,-30.5 23,-43.1c5.8,-7.8 16.6,-19.5 19,-20.7c4.7,-2.4 11.3,-1.2 15.2,2.7c5.4,5.4 5.2,13.9 -0.3,19.1c-4.3,4 -9.4,4.4 -12.6,0.9c-1.7,-1.9 -2.2,-3.9 -1.7,-6.4c0.2,-1.1 0.3,-2 0.2,-2.2c-0.3,-0.3 -3.6,3.3 -8.3,9.1c-17.6,21.8 -28.5,48 -31.9,76.5c-1.1,9.3 -1,26.4 0.1,34.6c0.3,1.8 0.8,1.9 1.4,0.1c0.9,-2.6 4,-4.7 6.8,-4.7c3,-0 5.9,2.2 7.5,5.7c0.6,1.3 0.8,2.3 0.8,5.2c-0,3.3 -0.1,3.8 -1.1,5.7c-1.4,2.7 -4.6,5.7 -7.1,6.6c-2.5,0.9 -6.1,1.2 -8,0.8Z" />
<path
android:fillColor="#31a452"
android:fillType="nonZero"
android:pathData="M790.1,691.5c-3.7,-0.6 -7.7,-3.6 -9.4,-7.1c-3.8,-7.5 0.1,-16.9 6.9,-16.9c3.1,0 5.8,2 6.9,5.2c0.4,1.2 0.5,1.3 0.7,0.7c1.3,-3.7 1.7,-26.4 0.6,-35.7c-3.6,-29.6 -14.5,-55.3 -33,-77.9c-5.5,-6.7 -8.4,-9.4 -7.1,-6.6c0.7,1.4 0.5,4.3 -0.3,5.9c-0.9,1.7 -3.2,3.5 -5,3.8c-3.2,0.6 -7.9,-1.6 -10.2,-4.8c-6.5,-8.8 -0.5,-21.2 10.4,-21.4c4.6,-0.1 5.2,0.3 11.2,6.4c12.1,12.3 21.1,24.9 28.8,40.3c13.2,26.3 18.6,54.9 16.1,84.5c-0.5,5.6 -2,15.7 -2.6,17.1c-1.3,2.8 -4.8,5.5 -8.4,6.5c-2.3,0.4 -3.1,0.4 -5.6,0Z" />
<path
android:fillColor="#4081ef"
android:fillType="nonZero"
android:pathData="M545.7,680.2c-6,-1.3 -12.2,-6.2 -14.9,-11.7c-3.4,-7 -3.1,-15.1 0.9,-21.6c0.7,-1.2 1.2,-2.3 1.1,-2.4c-0.1,-0.1 -1.1,-0.6 -2.1,-1c-3.9,-1.5 -8.1,-4.8 -10.7,-8.3c-4.6,-6.2 -6.1,-14.6 -3.9,-22.1c2.9,-10.3 9.4,-16.8 19.1,-19.3c2.8,-0.7 9,-0.8 11.7,-0c1.1,0.3 2.2,0.5 2.4,0.5c0.2,-0 0.3,-0.7 0.3,-1.5c0,-2.9 0.8,-5.8 2.4,-9.2c5.2,-10.8 18.1,-15.5 29,-10.5c2.7,1.2 6.2,3.8 7.8,5.8c0.7,0.8 10.3,14 21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1 -1.9,2.6 -2.5,3.5c-0.6,1 -1.2,1.7 -1.5,1.6c-4.5,-1.7 -46.7,-15 -47.7,-15c-1.9,0 -3.1,1.3 -3.1,3.2c0,1 0.2,1.7 0.8,2.3c0.6,0.6 7.8,3.1 24.5,8.5l23.7,7.7l-0.2,8.6l-32.6,10.5c-18,5.9 -33.9,10.9 -35.2,11.2c-3.1,0.7 -6.6,0.7 -9.6,0.1Z" />
<path
android:fillColor="#e64132"
android:fillType="nonZero"
android:pathData="M740,679.8c-1.8,-0.5 -17.5,-5.6 -35,-11.3l-31.8,-10.4l1,-4.3l0,-4.3l22.6,-7.7c15,-4.9 24,-8 24.6,-8.5c0.7,-0.6 0.9,-1.1 0.9,-2.2c-0,-2 -1.2,-3.3 -3.1,-3.3c-0.9,-0 -10.5,2.9 -24.7,7.5c-12.8,4.1 -23.4,7.5 -23.6,7.5c-0.1,-0 -0.7,-0.8 -1.3,-1.9c-0.6,-1 -1.6,-2.5 -2.2,-3.2c-0.7,-0.7 -1.2,-1.5 -1.2,-1.6c0,-0.2 9.6,-13.5 21.4,-29.6c18.9,-26 21.6,-29.6 23.6,-31.1c5.7,-4.4 13.1,-5.8 19.7,-3.9c9,2.7 16.1,11.6 16.1,20.3c0,2.3 -0.1,2.3 3.1,1.5c4.7,-1.1 11.5,-0.5 16,1.5c4.6,2 9,6 11.5,10.2c2.1,3.6 3.9,9.4 4.2,13.2c0.3,5.2 -1.1,10.7 -4,15.3c-2.6,4.1 -7.8,8.3 -12.1,9.8c-0.9,0.3 -1.7,0.8 -1.7,1c0,0.2 0.4,1 0.9,1.7c2.4,3.6 3.6,7.7 3.5,12.7c0,5.8 -2.1,10.7 -6.4,15.1c-4,4.1 -8.9,6.3 -14.9,6.5c-3.3,0.4 -4.3,0.3 -7.1,-0.5Z" />
<path
android:fillColor="#f2f5fb"
android:fillType="nonZero"
android:pathData="M643.7,671.9c-6.1,-1.6 -11.4,-6.8 -13.2,-12.9c-0.7,-2.4 -0.7,-7.5 0,-9.9c1.7,-5.8 6.6,-10.8 12.3,-12.5c2.7,-0.8 7.2,-0.9 10,-0.2c6.2,1.6 11.6,7.1 13.2,13.3c1.6,6 -0.3,12.6 -5,17.3c-4.6,4.6 -11.3,6.5 -17.3,4.9Z" />
<path
android:fillColor="#de7fb3"
android:fillType="nonZero"
android:pathData="M615.8,602.8c-13.3,-18.3 -21.2,-29.6 -22,-31.1c-1.4,-3 -1.9,-5.5 -1.9,-9.4c0,-14.1 13.1,-24.4 27.1,-21.4c1.4,0.3 2.6,0.5 2.7,0.5c0.1,0 0.3,-1.3 0.4,-2.8c0.8,-10.7 8.4,-19.6 18.9,-22.4c3.9,-1 10.6,-1 14.5,-0c8.9,2.3 15.9,9.3 18.2,18.2c0.4,1.5 0.7,3.7 0.7,4.9c-0,1.2 0.1,2.1 0.3,2.1c0.2,-0 1.5,-0.3 3,-0.6c7.4,-1.6 15.2,0.7 20.5,6c4.3,4.3 6.6,9.6 6.6,15.6c-0,4 -0.6,6.5 -2.4,10c-0.6,1.2 -10.4,15 -21.7,30.7c-17.8,24.5 -20.8,28.5 -21.4,28.3c-0.4,-0.1 -1.9,-0.6 -3.4,-1.1c-1.5,-0.5 -2.9,-0.9 -3.3,-0.9c-0.7,-0 -0.7,-0.8 -0.3,-25.5l-0,-25.5l-1.4,-0.9c-1,-1.1 -2.5,-1.5 -3.8,-0.9c-2,0.8 -2,-0.5 -1.8,27.2l-0,25.8l-1.2,-0c-0.5,-0.2 -2.4,0.3 -4,0.9c-1.6,0.6 -3.1,1.1 -3.2,1.1c-0.2,-0.1 -9.6,-13 -21.1,-28.8Z" />
<path
android:fillColor="#ffb800"
android:fillType="nonZero"
android:pathData="M578.4,537.8c-4.1,-0.9 -7.7,-3.6 -9.6,-7.4c-1.4,-2.8 -1.7,-7.3 -0.5,-10.3c1.7,-4.5 3.9,-6.1 15.6,-11.2c15.8,-7 31.4,-11.1 49.2,-12.9c7.3,-0.8 23.2,-0.8 30.6,0c17.4,1.8 33.3,6 49.1,13c7.3,3.2 12.5,6.1 13.6,7.5c4.3,5.6 3.8,12.7 -1.1,17.6c-5.1,5.1 -12.9,5.4 -18.1,0.7c-2,-1.8 -3,-3.5 -3.4,-5.6c-0.7,-4 2.9,-8.1 7.3,-8.2c1.4,0 1.5,-0.1 1.1,-0.5c-0.3,-0.3 -2.2,-1.2 -4.3,-2.1c-33.2,-14.5 -70.5,-16.4 -105,-5.4c-7.5,2.4 -19,7.2 -18.6,7.7c0.1,0.2 0.8,0.3 1.6,0.3c5.6,0 9.1,6.2 6.1,10.8c-2.9,4.5 -8.6,7.1 -13.6,6Z" />
<path
android:fillColor="#e64132"
android:fillType="nonZero"
android:pathData="M542.2,496.4c-8.9,-13.1 -16.8,-25.1 -17.5,-26.6c-1.6,-3.3 -3.6,-9.2 -4.4,-13c-2.6,-12.5 -0.9,-25.8 5,-37.5c4.2,-8.3 11.2,-16.3 18.6,-21.3c5,-3.4 6.1,-3.9 12.8,-6.3c23.1,-8.2 47.2,-13.1 73.4,-15c7.5,-0.6 28.5,-0.6 36.3,-0c25.5,1.8 50.6,6.9 73,14.8c6.4,2.2 8.2,3.1 13.1,6.5c9.8,6.6 18.1,17.5 22,29.2c2.2,6.5 2.7,10 2.7,17.9c0,7.9 -0.5,11.3 -2.7,17.9c-2.3,6.8 -3.7,9.1 -20.3,33.6l-16.1,23.8l-0.4,-2.2c-0.2,-1.2 -0.9,-3 -1.4,-4c-1,-1.8 -4.4,-5.6 -4.7,-5.2c-0.1,0.1 -1.2,-0.4 -2.4,-1.1c-9.1,-5.2 -21.9,-10.5 -33.2,-13.9c-37,-11 -77.2,-8.8 -113,6.1c-4.9,2.1 -17.7,8.4 -19.2,9.5c-2.2,1.6 -5.1,6.8 -5.1,9c0,0.4 -0.1,1 -0.3,1.2c0.1,0.2 -6.2,-8.8 -16.2,-23.4Z" />
</vector>

View File

@@ -0,0 +1,70 @@
<vector android:height="200dp"
android:viewportHeight="1300"
android:viewportWidth="1300"
android:width="200dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M578,922.6c-2.2,-0.2 -5.5,-1 -9.7,-2.2c-52.4,-15.7 -99,-46.5 -133.8,-88.5c-8.8,-10.7 -17.2,-22.4 -19.4,-27.5c-8.1,-18.1 -6.3,-38.7 4.8,-55.4c5,-7.5 13.2,-15 20.5,-18.7c1.2,-0.6 54.1,-20 55.8,-20.4c0.5,-0.1 0.5,0.2 -0.3,2.1c-0.7,1.7 -1,3.1 -1.1,5.5l-0.1,3.2l2.8,5.8c8.7,17.9 19.2,32.7 33.2,46.4c6.3,6.2 7.8,7.6 13.8,12.3c22.7,18.1 52,30.7 79.9,34.3c2.5,0.3 5,0.8 5.7,1c2.8,0.9 7.7,-0.8 11,-3.7l1.8,-1.6l-0.2,4.8c-0.1,2.7 -0.6,15.4 -1,28.3c-0.6,20.3 -0.8,24 -1.5,27.5c-3.9,20.7 -18.6,37.5 -38.4,44.1c-4.6,1.5 -8,2.2 -13.1,2.7c-4.6,0.5 -5.9,0.4 -10.7,0Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M707.3,922.4c-4,-0.4 -9.4,-1.6 -13.2,-2.9c-3.4,-1.2 -10,-4.4 -12.5,-6.1c-10.9,-7.4 -19,-17.9 -23.1,-30c-2.2,-6.7 -2.3,-7.5 -3.3,-36.9c-0.5,-14.9 -0.9,-27.9 -0.9,-28.9l-0,-1.9l2.3,1.8c2.6,2 6.6,3.4 8.5,3.1c0.6,-0.1 3,-0.5 5.3,-0.8c37.7,-5.3 71.2,-22.2 97.4,-49.1c12.2,-12.5 21.4,-25.5 29.9,-42.4l3.5,-7l0,-3.6c0,-3.1 -0.1,-3.8 -1,-5.7c-0.5,-1.2 -0.9,-2.1 -0.9,-2.2c0.2,-0.2 55.3,20.1 56.9,20.9c2.6,1.3 6.6,4.1 9.9,7c9.2,7.7 16.1,19.4 18.8,31.8c0.7,3.1 0.8,4.8 0.8,11.3c0,8.6 -0.5,11.7 -2.9,18.7c-1.7,5 -2.9,7.2 -7.1,13.1c-7.6,11 -15.3,20.5 -25.2,31.2c-32.8,35.4 -76.5,62.5 -123.4,76.3c-8,2.5 -12.4,3 -19.8,2.3Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M623.1,811c-25.9,-4.2 -50.7,-14.9 -71.7,-31c-5.2,-4 -8.7,-7.1 -14.1,-12.4c-12.7,-12.5 -21.9,-24.9 -30.5,-41.4c-2.3,-4.4 -2.4,-4.7 -2.4,-7.1c0,-8.8 8.5,-15.2 16.9,-12.7c5.6,1.7 9.6,6.8 9.7,12.2c0,2.6 -0.8,4.6 -2.6,6.2c-1.2,1.1 -3.2,1.9 -4.6,1.9c-1.2,0 -3.3,-0.8 -4.3,-1.6c-2.1,-1.8 -2,-1 0.4,3.2c19.3,33.8 52.3,59.1 90,69.1c5.7,1.5 11.5,2.7 11.8,2.4c0.1,-0.1 -0.4,-0.8 -1.3,-1.6c-5.1,-4.5 -2.3,-11.7 5,-12.8c5.4,-0.8 11.4,2.7 13.9,8c0.8,1.7 1,2.5 1,5.3c0,2.8 -0.1,3.5 -1,5.3c-2,4.3 -6.8,7.9 -10.3,7.8c-0.9,-0.1 -3.6,-0.5 -5.9,-0.8Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M665.1,811.2c-3.4,-1.3 -6.4,-4.3 -7.8,-8.1c-1.1,-2.9 -0.9,-7.3 0.5,-10.2c2.6,-5.3 8.7,-8.5 14.4,-7.5c2.9,0.5 4.7,1.9 6,4.3c0.8,1.6 1,2.2 0.8,3.6c-0.3,2.2 -0.9,3.3 -2.7,4.8c-0.8,0.7 -1.4,1.4 -1.3,1.5c0.5,0.5 13.4,-2.7 21.3,-5.4c33.6,-11.3 62.5,-35.1 80.4,-66.1c2.5,-4.4 2.6,-5 0.5,-3.2c-2.8,2.4 -7,1.9 -9.6,-1c-4,-4.6 -0.7,-13.8 6.1,-16.9c2,-0.9 2.7,-1 5.5,-1c2.9,0 3.5,0.1 5.6,1.1c4.4,2.1 7.4,6.4 7.8,11c0.2,2.2 0.1,2.3 -2.2,6.9c-23,45.9 -67,78.1 -117.2,85.9c-5.5,0.9 -6.3,1 -8.1,0.3Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M578.6,771.5c-4.7,-0.9 -8.7,-2.7 -12.9,-5.9c-10.8,-8.1 -13.5,-22.3 -6.6,-33.7c0.7,-1.2 1.1,-2.2 1,-2.4c-0.2,-0.2 -1.2,-0.6 -2.3,-1.1c-7.6,-3 -13,-10.6 -13.5,-19.1c-0.5,-7.4 3.1,-15 9,-19.4c1,-0.7 2.2,-1.5 2.6,-1.8c0.8,-0.4 68.9,-22.7 69.4,-22.7c0.2,0 0.7,0.7 1.2,1.5c0.5,0.8 1.6,2.3 2.4,3.3c1.2,1.4 1.5,1.9 1.2,2.3c-0.2,0.3 -6.9,9.5 -14.8,20.5c-15.9,21.9 -15.5,21.3 -13.4,23.4c1.3,1.3 2.9,1.4 4.4,0.3c0.6,-0.4 7.5,-9.7 15.5,-20.7c11.2,-15.4 14.6,-19.9 15,-19.7c0.9,0.4 5.5,1.9 6.6,2.1l1,0.2l-0,35.3c-0,39.7 -0,38.8 -2.5,44c-2.6,5.3 -7.2,9.3 -12.7,11.2c-3.7,1.3 -6.8,1.6 -10.2,1c-5.5,-0.9 -9.8,-3.2 -13.7,-7.4l-2.2,-2.4l-0.6,0.9c-3,4.3 -8.6,8.1 -14,9.5c-2.8,0.9 -7.8,1.2 -9.9,0.8Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M710.4,771.5c-5.5,-0.9 -9.9,-3.2 -14.3,-7.6l-3.2,-3.2l-0.7,1c-2.3,3.3 -6.8,6.5 -11.1,7.9c-3.7,1.2 -9.2,1.4 -12.6,0.3c-7.1,-2.1 -12.7,-7.4 -15.2,-14.3l-0.9,-2.6l0,-74.2l1.8,-0.4c1,-0.2 2.7,-0.8 3.9,-1.2c1.1,-0.5 2.1,-0.8 2.2,-0.7c0.1,0.1 6.5,9 14.4,19.9c7.8,10.9 14.7,20.1 15.2,20.5c2.2,1.9 5.4,0.4 5.4,-2.6c0,-1.4 -1,-2.9 -13.8,-20.5c-7.6,-10.5 -14.2,-19.6 -14.7,-20.4l-0.9,-1.3l1.4,-1.7c0.8,-0.9 1.9,-2.5 2.5,-3.4l1,-1.6l34.4,11.2c18.9,6.2 35.1,11.6 35.9,12.1c6.8,4 11.1,11.3 11.1,19.1c0,4.1 -0.5,6.4 -2.4,10.2c-2,4.1 -5.5,7.6 -9.6,9.7c-1.6,0.8 -3.2,1.5 -3.4,1.5c-1,0 -0.9,0.7 0.3,2.6c2.8,4.3 4,8.5 3.9,13.7c0,8.1 -3.7,15.2 -10.6,20.3c-6.5,4.8 -13.4,6.7 -20,5.7Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M421.4,714.9c-0.5,-0.1 -2.3,-0.4 -3.9,-0.7c-15.6,-2.6 -30.4,-12.6 -38.8,-26.2c-3.5,-5.7 -6.4,-13.2 -7.8,-19.9c-1.2,-6.1 -0.8,-28.1 0.8,-43.1c4.5,-43 19,-84.3 42.2,-120.7c6.5,-10.2 14.9,-21.5 18.2,-24.6c17.8,-16.6 43.1,-20.5 64.8,-10c4.3,2.1 8.8,5.1 12.7,8.6c2.8,2.4 5.8,6.1 20.9,25.5c9.7,12.5 17.8,22.8 17.9,23c0.2,0.2 -0.9,0.4 -3.2,0.4c-2.5,0 -4.1,0.2 -5.7,0.7c-2.1,0.7 -2.6,1.1 -7.9,6.3c-8.2,8.1 -14.4,15.3 -20.3,23.9c-15.5,22.2 -25.4,47.7 -28.8,74.8c-2.2,16.9 -1.6,37.5 1.6,52.3c0.3,1.4 0.5,2.8 0.4,3c-0.1,0.2 0.2,1.3 0.8,2.4c1.1,2.4 4.3,5.7 6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2 -13.1,3.8 -27.6,8c-16.4,4.7 -27.7,7.8 -29.8,8.1c-3.1,0.4 -11.1,0.6 -13.3,0.2Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M862.2,714.7c-2.1,-0.3 -33.8,-9.1 -56.5,-15.8l-2.5,-0.7l1.6,-0.8c3.4,-1.7 7.2,-6.6 7.3,-9.6c0,-0.7 0.4,-3.3 0.8,-5.8c3.9,-22.7 3.1,-46.1 -2.5,-68.4c-6.4,-25.5 -18.6,-49.2 -35.8,-69.1c-4.6,-5.3 -14.8,-15.4 -16.4,-16.1c-2.4,-1.1 -5.1,-1.6 -8,-1.4l-2.7,0.2l1.2,-1.5c0.7,-0.8 8.5,-10.8 17.5,-22.3c8.9,-11.5 17.2,-21.8 18.5,-23.1c2.6,-2.7 7,-6.2 10.3,-8.2c19.3,-11.6 43,-11.1 61.6,1.2c5.4,3.6 8.2,6.2 12.3,11.7c26.4,34.5 44,73.7 52.3,116.2c3.4,17.6 4.9,33.3 5,52.4c0,13 -0.2,14.8 -2.5,21.8c-8.4,26.2 -34.5,42.8 -61.5,39.3Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M501.4,691.5c-2,-0.5 -4.6,-1.9 -6,-3.3c-2.5,-2.4 -3.1,-3.5 -3.7,-7.3c-4.4,-27.3 -2.2,-54 6.7,-79.3c5.3,-15.1 13.5,-30.5 23,-43.1c5.8,-7.8 16.6,-19.5 19,-20.7c4.7,-2.4 11.3,-1.2 15.2,2.7c5.4,5.4 5.2,13.9 -0.3,19.1c-4.3,4 -9.4,4.4 -12.6,0.9c-1.7,-1.9 -2.2,-3.9 -1.7,-6.4c0.2,-1.1 0.3,-2 0.2,-2.2c-0.3,-0.3 -3.6,3.3 -8.3,9.1c-17.6,21.8 -28.5,48 -31.9,76.5c-1.1,9.3 -1,26.4 0.1,34.6c0.3,1.8 0.8,1.9 1.4,0.1c0.9,-2.6 4,-4.7 6.8,-4.7c3,-0 5.9,2.2 7.5,5.7c0.6,1.3 0.8,2.3 0.8,5.2c-0,3.3 -0.1,3.8 -1.1,5.7c-1.4,2.7 -4.6,5.7 -7.1,6.6c-2.5,0.9 -6.1,1.2 -8,0.8Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M790.1,691.5c-3.7,-0.6 -7.7,-3.6 -9.4,-7.1c-3.8,-7.5 0.1,-16.9 6.9,-16.9c3.1,0 5.8,2 6.9,5.2c0.4,1.2 0.5,1.3 0.7,0.7c1.3,-3.7 1.7,-26.4 0.6,-35.7c-3.6,-29.6 -14.5,-55.3 -33,-77.9c-5.5,-6.7 -8.4,-9.4 -7.1,-6.6c0.7,1.4 0.5,4.3 -0.3,5.9c-0.9,1.7 -3.2,3.5 -5,3.8c-3.2,0.6 -7.9,-1.6 -10.2,-4.8c-6.5,-8.8 -0.5,-21.2 10.4,-21.4c4.6,-0.1 5.2,0.3 11.2,6.4c12.1,12.3 21.1,24.9 28.8,40.3c13.2,26.3 18.6,54.9 16.1,84.5c-0.5,5.6 -2,15.7 -2.6,17.1c-1.3,2.8 -4.8,5.5 -8.4,6.5c-2.3,0.4 -3.1,0.4 -5.6,0Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M545.7,680.2c-6,-1.3 -12.2,-6.2 -14.9,-11.7c-3.4,-7 -3.1,-15.1 0.9,-21.6c0.7,-1.2 1.2,-2.3 1.1,-2.4c-0.1,-0.1 -1.1,-0.6 -2.1,-1c-3.9,-1.5 -8.1,-4.8 -10.7,-8.3c-4.6,-6.2 -6.1,-14.6 -3.9,-22.1c2.9,-10.3 9.4,-16.8 19.1,-19.3c2.8,-0.7 9,-0.8 11.7,-0c1.1,0.3 2.2,0.5 2.4,0.5c0.2,-0 0.3,-0.7 0.3,-1.5c0,-2.9 0.8,-5.8 2.4,-9.2c5.2,-10.8 18.1,-15.5 29,-10.5c2.7,1.2 6.2,3.8 7.8,5.8c0.7,0.8 10.3,14 21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1 -1.9,2.6 -2.5,3.5c-0.6,1 -1.2,1.7 -1.5,1.6c-4.5,-1.7 -46.7,-15 -47.7,-15c-1.9,0 -3.1,1.3 -3.1,3.2c0,1 0.2,1.7 0.8,2.3c0.6,0.6 7.8,3.1 24.5,8.5l23.7,7.7l-0.2,8.6l-32.6,10.5c-18,5.9 -33.9,10.9 -35.2,11.2c-3.1,0.7 -6.6,0.7 -9.6,0.1Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M740,679.8c-1.8,-0.5 -17.5,-5.6 -35,-11.3l-31.8,-10.4l1,-4.3l0,-4.3l22.6,-7.7c15,-4.9 24,-8 24.6,-8.5c0.7,-0.6 0.9,-1.1 0.9,-2.2c-0,-2 -1.2,-3.3 -3.1,-3.3c-0.9,-0 -10.5,2.9 -24.7,7.5c-12.8,4.1 -23.4,7.5 -23.6,7.5c-0.1,-0 -0.7,-0.8 -1.3,-1.9c-0.6,-1 -1.6,-2.5 -2.2,-3.2c-0.7,-0.7 -1.2,-1.5 -1.2,-1.6c0,-0.2 9.6,-13.5 21.4,-29.6c18.9,-26 21.6,-29.6 23.6,-31.1c5.7,-4.4 13.1,-5.8 19.7,-3.9c9,2.7 16.1,11.6 16.1,20.3c0,2.3 -0.1,2.3 3.1,1.5c4.7,-1.1 11.5,-0.5 16,1.5c4.6,2 9,6 11.5,10.2c2.1,3.6 3.9,9.4 4.2,13.2c0.3,5.2 -1.1,10.7 -4,15.3c-2.6,4.1 -7.8,8.3 -12.1,9.8c-0.9,0.3 -1.7,0.8 -1.7,1c0,0.2 0.4,1 0.9,1.7c2.4,3.6 3.6,7.7 3.5,12.7c0,5.8 -2.1,10.7 -6.4,15.1c-4,4.1 -8.9,6.3 -14.9,6.5c-3.3,0.4 -4.3,0.3 -7.1,-0.5Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M643.7,671.9c-6.1,-1.6 -11.4,-6.8 -13.2,-12.9c-0.7,-2.4 -0.7,-7.5 0,-9.9c1.7,-5.8 6.6,-10.8 12.3,-12.5c2.7,-0.8 7.2,-0.9 10,-0.2c6.2,1.6 11.6,7.1 13.2,13.3c1.6,6 -0.3,12.6 -5,17.3c-4.6,4.6 -11.3,6.5 -17.3,4.9Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M615.8,602.8c-13.3,-18.3 -21.2,-29.6 -22,-31.1c-1.4,-3 -1.9,-5.5 -1.9,-9.4c0,-14.1 13.1,-24.4 27.1,-21.4c1.4,0.3 2.6,0.5 2.7,0.5c0.1,0 0.3,-1.3 0.4,-2.8c0.8,-10.7 8.4,-19.6 18.9,-22.4c3.9,-1 10.6,-1 14.5,-0c8.9,2.3 15.9,9.3 18.2,18.2c0.4,1.5 0.7,3.7 0.7,4.9c-0,1.2 0.1,2.1 0.3,2.1c0.2,-0 1.5,-0.3 3,-0.6c7.4,-1.6 15.2,0.7 20.5,6c4.3,4.3 6.6,9.6 6.6,15.6c-0,4 -0.6,6.5 -2.4,10c-0.6,1.2 -10.4,15 -21.7,30.7c-17.8,24.5 -20.8,28.5 -21.4,28.3c-0.4,-0.1 -1.9,-0.6 -3.4,-1.1c-1.5,-0.5 -2.9,-0.9 -3.3,-0.9c-0.7,-0 -0.7,-0.8 -0.3,-25.5l-0,-25.5l-1.4,-0.9c-1,-1.1 -2.5,-1.5 -3.8,-0.9c-2,0.8 -2,-0.5 -1.8,27.2l-0,25.8l-1.2,-0c-0.5,-0.2 -2.4,0.3 -4,0.9c-1.6,0.6 -3.1,1.1 -3.2,1.1c-0.2,-0.1 -9.6,-13 -21.1,-28.8Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M578.4,537.8c-4.1,-0.9 -7.7,-3.6 -9.6,-7.4c-1.4,-2.8 -1.7,-7.3 -0.5,-10.3c1.7,-4.5 3.9,-6.1 15.6,-11.2c15.8,-7 31.4,-11.1 49.2,-12.9c7.3,-0.8 23.2,-0.8 30.6,0c17.4,1.8 33.3,6 49.1,13c7.3,3.2 12.5,6.1 13.6,7.5c4.3,5.6 3.8,12.7 -1.1,17.6c-5.1,5.1 -12.9,5.4 -18.1,0.7c-2,-1.8 -3,-3.5 -3.4,-5.6c-0.7,-4 2.9,-8.1 7.3,-8.2c1.4,0 1.5,-0.1 1.1,-0.5c-0.3,-0.3 -2.2,-1.2 -4.3,-2.1c-33.2,-14.5 -70.5,-16.4 -105,-5.4c-7.5,2.4 -19,7.2 -18.6,7.7c0.1,0.2 0.8,0.3 1.6,0.3c5.6,0 9.1,6.2 6.1,10.8c-2.9,4.5 -8.6,7.1 -13.6,6Z" />
<path
android:fillColor="#fff"
android:fillType="nonZero"
android:pathData="M542.2,496.4c-8.9,-13.1 -16.8,-25.1 -17.5,-26.6c-1.6,-3.3 -3.6,-9.2 -4.4,-13c-2.6,-12.5 -0.9,-25.8 5,-37.5c4.2,-8.3 11.2,-16.3 18.6,-21.3c5,-3.4 6.1,-3.9 12.8,-6.3c23.1,-8.2 47.2,-13.1 73.4,-15c7.5,-0.6 28.5,-0.6 36.3,-0c25.5,1.8 50.6,6.9 73,14.8c6.4,2.2 8.2,3.1 13.1,6.5c9.8,6.6 18.1,17.5 22,29.2c2.2,6.5 2.7,10 2.7,17.9c0,7.9 -0.5,11.3 -2.7,17.9c-2.3,6.8 -3.7,9.1 -20.3,33.6l-16.1,23.8l-0.4,-2.2c-0.2,-1.2 -0.9,-3 -1.4,-4c-1,-1.8 -4.4,-5.6 -4.7,-5.2c-0.1,0.1 -1.2,-0.4 -2.4,-1.1c-9.1,-5.2 -21.9,-10.5 -33.2,-13.9c-37,-11 -77.2,-8.8 -113,6.1c-4.9,2.1 -17.7,8.4 -19.2,9.5c-2.2,1.6 -5.1,6.8 -5.1,9c0,0.4 -0.1,1 -0.3,1.2c0.1,0.2 -6.2,-8.8 -16.2,-23.4Z" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 65,
"android.injected.version.name" => "1.42.0",
"android.injected.version.code" => 67,
"android.injected.version.name" => "1.44.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

@@ -0,0 +1,7 @@
* Fix crash at first start related to uninitialized hive key
* Fix invalid creation time on local asset show 1970 as year
* Fix Home page app bar icons don't conform to theme change
* Fix endless 'Building timeline' loop after changing the number of assets per row
* Show current upload asset
* Add to album from asset detail view
* Add multi selected assets to album

View File

@@ -118,6 +118,7 @@
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Angiv venligst http:// eller https://",
"login_form_err_invalid_email": "Ugyldig email",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Mellemrum før",
"login_form_err_trailing_whitespace": "Mellemrum efter",
"login_form_failed_get_oauth_server_config": "Fejl med at logge på med OAuth. Tjek serveres URL",
@@ -185,10 +186,10 @@
"theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "Tre-trins indlæsning kan øge ydeevnen, men kan ligeledes føre til højere netværksbelastning",
"theme_setting_three_stage_loading_title": "Slå tre-trins indlæsning til",
"version_announcement_overlay_ack": "Vedkend",
"version_announcement_overlay_release_notes": "udgivelsesnoter",
"version_announcement_overlay_text_1": "Hej vej, der er en ny version af",
"version_announcement_overlay_text_2": "bresøg venligst ",
"version_announcement_overlay_text_3": " og sikker dig, at din dockercompose og .env-fil er opdateret, for at undgå fejlkonfiguration, specielt hvis u bruger WatchTowereller andre mekanisme, der automatisk opdaterer serverprogrammer.",
"version_announcement_overlay_ack": "Accepter",
"version_announcement_overlay_release_notes": "udgivelsesnoterne",
"version_announcement_overlay_text_1": "Hej ven, der er en ny version af",
"version_announcement_overlay_text_2": ". Besøg venligst ",
"version_announcement_overlay_text_3": " for at sikre dig, at din dockercompose- og .env-fil er opdateret, så der undgås fejlkonfiguration, specielt hvis du bruger WatchTower eller lignede.",
"version_announcement_overlay_title": "Ny serverversion er tilgængelig \uD83C\uDF89"
}

View File

@@ -86,7 +86,7 @@
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_create_new_album": "Neues Album erstellen",
"control_bottom_app_bar_delete": "Löschen",
"control_bottom_app_bar_share": "Teilen",
"create_album_page_untitled": "Unbenannt",
@@ -118,6 +118,7 @@
"login_form_endpoint_url": "Server URL",
"login_form_err_http": "Bitte gebe http:// oder https:// an",
"login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",

View File

@@ -62,7 +62,7 @@
"backup_controller_page_status_off": "Automatic foreground backup is off",
"backup_controller_page_status_on": "Automatic foreground backup is on",
"backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "Albums to be backed up",
"backup_controller_page_to_backup": "Albums to be backup",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
"backup_controller_page_turn_off": "Turn off foreground backup",
@@ -110,6 +110,8 @@
"experimental_settings_title": "Experimental",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"login_form_button_text": "Login",
@@ -118,6 +120,7 @@
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Please specify http:// or https://",
"login_form_err_invalid_email": "Invalid Email",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Leading whitespace",
"login_form_err_trailing_whitespace": "Trailing whitespace",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
@@ -191,4 +194,4 @@
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
}
}

View File

@@ -1,96 +1,96 @@
{
"album_info_card_backup_album_excluded": "EXCLUIDOS",
"album_info_card_backup_album_included": "INCLUIDOS",
"album_thumbnail_card_item": "1 elemento",
"album_thumbnail_card_items": "{} elementos",
"album_thumbnail_card_shared": " · Compartido",
"album_viewer_appbar_share_delete": "Eliminar álbum",
"album_viewer_appbar_share_err_delete": "No se ha podido eliminar el álbum",
"album_viewer_appbar_share_err_leave": "No se ha podido dejar el álbum",
"album_viewer_appbar_share_err_remove": "Hay problemas para eliminar contenidos del álbum",
"album_viewer_appbar_share_err_title": "No se ha podido cambiar el título del álbum",
"album_viewer_appbar_share_leave": "Abandonar álbum",
"album_viewer_appbar_share_remove": "Eliminar del álbum",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"album_viewer_appbar_share_delete": "Eliminar álbum ",
"album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum",
"album_viewer_appbar_share_err_leave": "No ha podido dejar el álbum",
"album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los activos del álbum",
"album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ",
"album_viewer_appbar_share_leave": "Abandonar álbum ",
"album_viewer_appbar_share_remove": "Eliminar del álbum ",
"album_viewer_page_share_add_users": "Añadir usuarios",
"asset_list_settings_subtitle": "Configuración de la galería",
"asset_list_settings_title": "Galería",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
"backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
"backup_album_selection_page_assets_scatter": "Los elementos pueden estar en varios álbumes. Por eso, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
"backup_album_selection_page_select_albums": "Seleccionar álbumes",
"backup_album_selection_page_selection_info": "Información sobre la selección",
"backup_album_selection_page_total_assets": "Total de elementos únicos",
"backup_album_selection_page_assets_scatter": "Los activos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
"backup_album_selection_page_select_albums": "Seleccionar Álbumes",
"backup_album_selection_page_selection_info": "Información sobre la Selección",
"backup_album_selection_page_total_assets": "Total de activos únicos",
"backup_all": "Todos",
"backup_background_service_backup_failed_message": "No se ha podido realizar la copia de seguridad. Reintentando…",
"backup_background_service_connection_failed_message": "No se ha podido conectar con el servidor. Reintentando…",
"backup_background_service_current_upload_notification": "Subidendo {}",
"backup_background_service_default_notification": "Buscando nuevos elementos…",
"backup_background_service_error_title": "Error de copia de seguridad",
"backup_background_service_in_progress_notification": "Subiendo elementos…",
"backup_background_service_upload_failure_notification": "Error al subir {}",
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_error_title": "Backup error",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "Álbumes de copia de seguridad",
"backup_controller_page_background_battery_info_link": "Muéstrame cómo",
"backup_controller_page_background_battery_info_message": "Para disfrutar de la mejor experiencia de copia de seguridad en segundo plano, desactive cualquier optimización de la batería que restrinja la actividad en segundo plano para Immich.\nDado que esto es específico de cada dispositivo, busque la información necesaria para el fabricante de su dispositivo.",
"backup_controller_page_background_battery_info_link": "Show me how",
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Optimización de batería",
"backup_controller_page_background_charging": "Sólo mientras carga",
"backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano",
"backup_controller_page_background_battery_info_title": "Battery optimizations",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_description": "Activar el servicio en segundo plano para realizar copias de seguridad automáticas de los nuevos elementos sin tener que abrir la aplicación",
"backup_controller_page_background_is_off": "Copia de seguridad automática apagada",
"backup_controller_page_background_is_on": "Copia de seguridad automática encendida",
"backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano",
"backup_controller_page_background_turn_on": "Activar el servicio en segundo plano",
"backup_controller_page_background_wifi": "Sólo con WiFi",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_wifi": "Only on WiFi",
"backup_controller_page_backup": "Copia de Seguridad",
"backup_controller_page_backup_selected": "Seleccionado:",
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
"backup_controller_page_cancel": "Cancelar",
"backup_controller_page_created": "Creado el: {}",
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos elementos al servidor.",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
"backup_controller_page_excluded": "Excluido:",
"backup_controller_page_failed": "Fallido ({})",
"backup_controller_page_filename": "Nombre de archivo: {} [{}]",
"backup_controller_page_failed": "Failed ({})",
"backup_controller_page_filename": "File name: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Información de la copia de seguridad",
"backup_controller_page_info": "Información de la Copia de Seguridad",
"backup_controller_page_none_selected": "Ninguno seleccionado",
"backup_controller_page_remainder": "Restantes",
"backup_controller_page_remainder": "Remanente",
"backup_controller_page_remainder_sub": "Fotos y álbumes restantes para hacer una copia de seguridad de la selección",
"backup_controller_page_select": "Seleccionar",
"backup_controller_page_server_storage": "Almacenamiento en el servidor",
"backup_controller_page_start_backup": "Iniciar copia de seguridad",
"backup_controller_page_status_off": "La copia de seguridad está desactivada",
"backup_controller_page_status_on": "La copia de seguridad está activada",
"backup_controller_page_storage_format": "{} de {} usados",
"backup_controller_page_storage_format": "{} de {} usadas",
"backup_controller_page_to_backup": "Álbumes a respaldar",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
"backup_controller_page_turn_on": "Activar la copia de seguridad",
"backup_controller_page_uploading_file_info": "SUbiendo información de archivos",
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "No se puede eliminar el único álbum",
"backup_info_card_assets": "elementos",
"cach_settings_album_thumbnails": "Miniaturas del álbum ({} elementos)",
"cache_settings_clear_cache_button": "Limpiar caché",
"cache_settings_clear_cache_button_title": "Limpia el caché de la app. Esto afectará significativamente al rendimiento de la aplicación hasta que la caché se haya reconstruido.",
"cache_settings_image_cache_size": "Miniaturas de imágenes ({} elementos)",
"cache_settings_statistics_album": "Miniaturas del álbum",
"cache_settings_statistics_assets": "{} elementos ({})",
"cache_settings_statistics_full": "Imágenes completas",
"cache_settings_statistics_shared": "Miniaturas del álbum compartido",
"cache_settings_statistics_thumbnail": "Miniaturas",
"cache_settings_statistics_title": "Uso de caché",
"cache_settings_subtitle": "Controla el comportamiento de almacenamiento en caché de la aplicación de Immich",
"cache_settings_thumbnail_size": "Tamaño del caché de miniaturas ({} elementos)",
"cache_settings_title": "Ajustes del caché",
"backup_info_card_assets": "activos",
"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_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "Full images",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Thumbnails",
"cache_settings_statistics_title": "Cache usage",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_title": "Caching Settings",
"control_bottom_app_bar_add_to_album": "Añadir al álbum",
"control_bottom_app_bar_album_info": "{} elementos",
"control_bottom_app_bar_album_info_shared": "{} elementos · Compartido",
"control_bottom_app_bar_create_new_album": "Crear nuevo álbum",
"control_bottom_app_bar_delete": "Eliminar",
"control_bottom_app_bar_share": "Compartir",
"create_album_page_untitled": "Sin título",
"create_shared_album_page_create": "Crear",
"control_bottom_app_bar_share": "Share",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share": "Compartir",
"create_shared_album_page_share_add_assets": "AÑADIR ACTIVOS",
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
@@ -101,94 +101,95 @@
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Eliminar",
"delete_dialog_title": "Eliminar Permanentemente",
"exif_bottom_sheet_description": "Añadir descripción...",
"exif_bottom_sheet_description": "Añadir Descripción...",
"exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "LOCALZACIÓN",
"experimental_settings_new_asset_list_subtitle": "En desarrollo",
"experimental_settings_new_asset_list_title": "Habilitar galería experimental",
"experimental_settings_subtitle": "¡Úsalo bajo tu responsabilidad!",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"home_page_add_to_album_conflicts": "Añadidos {added} elementos al álbum {album}. {failed} elementos ya estaban añadidos.",
"home_page_add_to_album_conflicts": "Añadidos {added} elementos al álbum {album}. {failed} elementos ya están añadidos.",
"home_page_add_to_album_success": "Añadidos {added} elementos al álbum {album}.",
"library_page_albums": "Álbumes",
"library_page_new_album": "Nuevo álbum",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"login_form_button_text": "Iniciar Sesión",
"login_form_email_hint": "tucorreo@correo.com",
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api",
"login_form_endpoint_url": "URL del servidor",
"login_form_err_http": "Por favor, especifique http:// o https://",
"login_form_err_invalid_email": "Correo electrónico no válido",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
"login_form_failed_get_oauth_server_config": "Fallo al iniciar sesión con OAuth. Comprueba la URL del servidor.",
"login_form_failed_get_oauth_server_disable": "OAuth no está disponible en este servidor",
"login_form_failed_login": "Fallo al iniciar sesión, comprueba URL, correo y contraseña",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
"login_form_failed_login": "Error logging you in, check server URL, email and password",
"login_form_label_email": "Correo",
"login_form_label_password": "Contraseña",
"login_form_password_hint": "contraseña",
"login_form_save_login": "Mantener la sesión iniciada",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_app_logs": "Registros",
"profile_drawer_client_server_up_to_date": "El cliente y el servidor están actualizados",
"profile_drawer_settings": "Ajustes",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
"profile_drawer_settings": "Settings",
"profile_drawer_sign_out": "Cerrar Sesión",
"search_bar_hint": "Buscar fotos",
"search_page_no_objects": "No hay información sobre objetos",
"search_page_no_places": "No hay información sobre lugares",
"search_bar_hint": "Busca tus fotos",
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No hay información de lugares disponibles",
"search_page_places": "Lugares",
"search_page_things": "Objetos",
"search_result_page_new_search_hint": "Nueva squeda",
"search_page_things": "Cosas",
"search_result_page_new_search_hint": "Nueva Busqueda",
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
"select_user_for_sharing_page_share_suggestions": "Sugerencias",
"setting_image_viewer_help": "El visor carga primero la miniatura pequeña, luego carga la de tamaño medio (si está activada) y por último la original (si está activada).",
"setting_image_viewer_original_subtitle": "Activar para cargar la imagen original (¡grande!). Desactivar para reducir el uso de datos (tanto de red como de caché).",
"setting_image_viewer_original_title": "Cargar imágenes originales",
"setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Desactivar para cargar directamente el original o utilizar sólo la miniatura.",
"setting_image_viewer_preview_title": "Cargar miniaturas",
"setting_notifications_notify_failures_grace_period": "Notificar fallos de carga en segundo plano: {}",
"setting_notifications_notify_hours": "{} horas",
"setting_notifications_notify_immediately": "inmediatamente",
"setting_notifications_notify_minutes": "{} minutos",
"setting_notifications_notify_never": "nunca",
"setting_notifications_notify_seconds": "{} segundos",
"setting_notifications_single_progress_subtitle": "Información detallada por cada elemento en copia",
"setting_notifications_single_progress_title": "Mostrar detalles de la subida",
"setting_notifications_subtitle": "Ajusta tus preferencias de notificación",
"setting_notifications_title": "Notificaciones",
"setting_notifications_total_progress_subtitle": "Progreso general de la subida (subidos/total elementos)",
"setting_notifications_total_progress_title": "Mostrar progreso de la subida",
"setting_pages_app_bar_settings": "Ajustes",
"settings_require_restart": "Por favor, reinica Immich para aplicar estos cambios",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Habilitar 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",
"setting_image_viewer_preview_subtitle": "Habilitar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.",
"setting_image_viewer_preview_title": "Cargar imagen de previsualización",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_immediately": "immediately",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "never",
"setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
"setting_notifications_single_progress_title": "Show background backup detail progress",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_title": "Notifications",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Añadir",
"share_add_photos": "Añadir fotos",
"share_add_title": "Añadir un título",
"share_create_album": "Crear álbum",
"share_dialog_preparing": "Preparando...",
"share_dialog_preparing": "Preparing...",
"share_invite": "Invitar al álbum",
"sharing_page_album": "Álbumes compartidos",
"sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con otros usuarios.",
"sharing_page_empty_list": "LISTA VACÍA",
"sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con las personas de tu red.",
"sharing_page_empty_list": "LISTA VACIA",
"sharing_silver_appbar_create_shared_album": "Crear un álbum compartido",
"sharing_silver_appbar_share_partner": "Compartir con pareja",
"tab_controller_nav_library": "Álbumes",
"tab_controller_nav_photos": "Galería",
"sharing_silver_appbar_share_partner": "Compartir con el compañero",
"tab_controller_nav_library": "Library",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Buscar",
"tab_controller_nav_sharing": "Compartido",
"theme_setting_asset_list_storage_indicator_title": "Mostrar estado de subida en cada elemento",
"theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})",
"theme_setting_dark_mode_switch": "Modo oscuro",
"theme_setting_image_viewer_quality_subtitle": "Ajusta la calidad del visor",
"theme_setting_image_viewer_quality_title": "Calidad del visor",
"theme_setting_system_theme_switch": "Automático (Según ajustes del sistema)",
"theme_setting_theme_subtitle": "Elige el tema de la app",
"theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "La carga en tres fases podría aumentar el rendimiento pero provoca una mayor carga de red",
"theme_setting_three_stage_loading_title": "Activar carga en tres fases",
"tab_controller_nav_sharing": "Compartiendo",
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_theme_title": "Theme",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"version_announcement_overlay_ack": "Reconocer",
"version_announcement_overlay_release_notes": "Notas de la versión",
"version_announcement_overlay_text_1": "¡Hola! Hay una nueva versión de",
"version_announcement_overlay_text_2": "tómate tu tiempo para visitar la ",
"version_announcement_overlay_text_3": "y asegúrate de que tu docker-compose y .env están actualizados para prevenir cualquier configuración errónea, especialmente si usas WatchTower o cualquier otro mecanismo que automatice la actualización.",
"version_announcement_overlay_release_notes": "notas de versión",
"version_announcement_overlay_text_1": "Hola amigo, hay una nueva versión de",
"version_announcement_overlay_text_2": "tómese su tiempo para visitar la ",
"version_announcement_overlay_text_3": "y asegurate de que tu configuración de docker-compose y .env está actualizada para evitar cualquier desconfiguración, especialmente si utiliza WatchTower o cualquier mecanismo que se encargue de actualizar su aplicación de servidor automáticamente.",
"version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89"
}
}

View File

@@ -35,7 +35,7 @@
"backup_controller_page_background_battery_info_title": "Akun optimointi",
"backup_controller_page_background_charging": "Vain laitteen ollessa kytkettynä laturiin",
"backup_controller_page_background_configure_error": "Taustapalvelun asettaminen epäonnistui",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_delay": "Viivästytä uusien kohteiden varmuuskopiointia: {}",
"backup_controller_page_background_description": "Kytke taustapalvelu päälle varmuuskopioidaksesi uudet kohteet automaattisesti, ilman sovelluksen avaamista",
"backup_controller_page_background_is_off": "Automaattinen varmuuskopiointi taustalla on pois päältä",
"backup_controller_page_background_is_on": "Automaattinen varmuuskopiointi taustalla on päällä",
@@ -83,10 +83,10 @@
"cache_settings_subtitle": "Hallitse Immich-mobiilisovelluksen välimuistin käyttöä",
"cache_settings_thumbnail_size": "Esikatselukuvien välimuistin koko ({} kohdetta)",
"cache_settings_title": "Välimuistin asetukset",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_add_to_album": "Lisää albumiin",
"control_bottom_app_bar_album_info": "{} kohdetta",
"control_bottom_app_bar_album_info_shared": "{} kohdetta · Jaettu",
"control_bottom_app_bar_create_new_album": "Luo uusi albumi",
"control_bottom_app_bar_delete": "Poista",
"control_bottom_app_bar_share": "Jaa",
"create_album_page_untitled": "Nimetön",
@@ -108,8 +108,8 @@
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
"experimental_settings_title": "Kokeellinen",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_add_to_album_conflicts": "Lisätty {added} kohdetta albumiin {album}. {failed} kohdetta on jo albumissa.",
"home_page_add_to_album_success": "Lisätty {added} kohdetta albumiin {album}.",
"library_page_albums": "Albumit",
"library_page_new_album": "Uusi albumi",
"login_form_button_text": "Kirjaudu",
@@ -118,17 +118,18 @@
"login_form_endpoint_url": "Palvelimen URL",
"login_form_err_http": "Lisää http:// tai https://",
"login_form_err_invalid_email": "Virheellinen sähköpostiosoite",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Alussa välilyönti",
"login_form_err_trailing_whitespace": "Lopussa välilyönti",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
"login_form_failed_get_oauth_server_config": "Virhe kirjauduttaessa OAuth:lla, tarkista palvelimen URL",
"login_form_failed_get_oauth_server_disable": "OAuth-ominaisuus ei ole käytössä tällä palvelimella",
"login_form_failed_login": "Virhe kirjautumisessa. Tarkista palvelimen URL, sähköpostiosoite ja salasana.",
"login_form_label_email": "Sähköposti",
"login_form_label_password": "Salasana",
"login_form_password_hint": "salasana",
"login_form_save_login": "Pysy kirjautuneena",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_app_logs": "Logs",
"profile_drawer_app_logs": "Lokit",
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
"profile_drawer_settings": "Asetukset",
"profile_drawer_sign_out": "Kirjaudu ulos",
@@ -141,17 +142,17 @@
"select_additional_user_for_sharing_page_suggestions": "Ehdotukset",
"select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui",
"select_user_for_sharing_page_share_suggestions": "Ehdotukset",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image",
"setting_image_viewer_help": "Sovellus lataa ensin pienen esikatselukuvan, toisena keskitarkkuuksisen kuvan (jos käytössä) ja kolmantena alkuperäisen täysitarkkuuksisen kuvan (jos käytössä)",
"setting_image_viewer_original_subtitle": "Ota käyttöön ladataksesi alkuperäinen täysitarkkuuksinen kuva (suuri!). Poista käytöstä vähentääksesi datan käyttöä (sekä verkossa että laitteen välimuistissa).",
"setting_image_viewer_original_title": "Lataa alkuperäinen kuva",
"setting_image_viewer_preview_subtitle": "Ota käyttöön ladataksesi keskitarkkuuksinen kuva. Poista käytöstä ladataksesi alkuperäinen kuva tai käyttääksesi vain esikatselukuvaa.",
"setting_image_viewer_preview_title": "Lataa esikatselukuva",
"setting_notifications_notify_failures_grace_period": "Ilmoita taustavarmuuskopioinnin epäonnistumisista: {}",
"setting_notifications_notify_hours": "{} tunnin välein",
"setting_notifications_notify_immediately": "heti",
"setting_notifications_notify_minutes": "{} minuutin välein",
"setting_notifications_notify_never": "ei koskaan",
"setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_notify_seconds": "{} sekuntia",
"setting_notifications_single_progress_subtitle": "Yksityiskohtainen tieto palvelimelle lähettämisen edistymisestä kohteittain",
"setting_notifications_single_progress_title": "Näytä taustavarmuuskopioinnin eidstminen",
"setting_notifications_subtitle": "Ilmoitusasetusten määrittely",

View File

@@ -118,6 +118,7 @@
"login_form_endpoint_url": "URL du point d'accès au serveur",
"login_form_err_http": "Veuillez préciser http:// ou https://",
"login_form_err_invalid_email": "Email invalide",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Espace en début de ligne",
"login_form_err_trailing_whitespace": "Espace de fin de ligne",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",

View File

@@ -118,6 +118,7 @@
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Per favore specificare http:// o https://",
"login_form_err_invalid_email": "Email non valida",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Whitespace all'inizio ",
"login_form_err_trailing_whitespace": "Whitespace alla fine",
"login_form_failed_get_oauth_server_config": "Errore di login usando OAuth, controlla l'URL del server",

View File

@@ -118,6 +118,7 @@
"login_form_endpoint_url": "サーバーエンドポイントURL",
"login_form_err_http": "http://かhttps://かを指定してね",
"login_form_err_invalid_email": "メールアドレスが有効じゃないよ",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "最初に半角スペースが含まれてるよ",
"login_form_err_trailing_whitespace": "最後に半角スペースが含まれてるよ",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",

View File

@@ -118,6 +118,7 @@
"login_form_endpoint_url": "URL Serwera",
"login_form_err_http": "Proszę określić http:// lub https://",
"login_form_err_invalid_email": "Niepoprawny Email",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Białe znaki",
"login_form_err_trailing_whitespace": "Białe znaki po przecinku",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",

View File

@@ -0,0 +1,45 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import '../test_utils/general_helper.dart';
void main() async {
await ImmichTestHelper.initialize();
group("Login input validation test", () {
immichWidgetTest("Test leading/trailing whitespace", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentials(
email: " demo@immich.app",
);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_leading_whitespace".tr()), findsOneWidget);
await helper.loginHelper.enterCredentials(
email: "demo@immich.app ",
);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_trailing_whitespace".tr()), findsOneWidget);
});
immichWidgetTest("Test invalid email", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentials(
email: "demo.immich.app",
);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_invalid_email".tr()), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter_test/flutter_test.dart';
import '../test_utils/general_helper.dart';
import '../test_utils/login_helper.dart';
void main() async {
await ImmichTestHelper.initialize();
group("Login tests", () {
immichWidgetTest("Test correct credentials", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentialsOf(
LoginCredentials.testInstance,
);
await helper.loginHelper.pressLoginButton();
await helper.loginHelper.assertLoginSuccess();
});
immichWidgetTest("Test login with wrong password", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentialsOf(
LoginCredentials.testInstanceButWithWrongPassword,
);
await helper.loginHelper.pressLoginButton();
await helper.loginHelper.assertLoginFailed();
});
immichWidgetTest("Test login with wrong server URL",
(tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentialsOf(
LoginCredentials.wrongInstanceUrl,
);
await helper.loginHelper.pressLoginButton();
await helper.loginHelper.assertLoginFailed();
});
});
}

View File

@@ -0,0 +1,58 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:integration_test/integration_test.dart';
// ignore: depend_on_referenced_packages
import 'package:meta/meta.dart';
import 'package:immich_mobile/main.dart' as app;
import 'login_helper.dart';
class ImmichTestHelper {
final WidgetTester tester;
ImmichTestHelper(this.tester);
ImmichTestLoginHelper? _loginHelper;
ImmichTestLoginHelper get loginHelper {
_loginHelper ??= ImmichTestLoginHelper(tester);
return _loginHelper!;
}
static Future<IntegrationTestWidgetsFlutterBinding> initialize() async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
// Load hive, localization...
await app.initApp();
return binding;
}
static Future<void> loadApp(WidgetTester tester) async {
// Clear all data from Hive
await Hive.deleteFromDisk();
await app.openBoxes();
// Load main Widget
await tester.pumpWidget(app.getMainWidget());
// Post run tasks
await tester.pumpAndSettle();
await EasyLocalization.ensureInitialized();
}
}
@isTest
void immichWidgetTest(
String description,
Future<void> Function(WidgetTester, ImmichTestHelper) test,
) {
testWidgets(
description,
(widgetTester) async {
await ImmichTestHelper.loadApp(widgetTester);
await test(widgetTester, ImmichTestHelper(widgetTester));
},
semanticsEnabled: false,
);
}

View File

@@ -0,0 +1,121 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class ImmichTestLoginHelper {
final WidgetTester tester;
ImmichTestLoginHelper(this.tester);
Future<void> waitForLoginScreen({int timeoutSeconds = 20}) async {
for (var i = 0; i < timeoutSeconds; i++) {
// Search for "IMMICH" test in the app bar
final result = find.text("IMMICH");
if (tester.any(result)) {
// Wait 5s until everything settled
await tester.pump(const Duration(seconds: 5));
return;
}
// Wait 1s before trying again
await Future.delayed(const Duration(seconds: 1));
}
fail("Timeout while waiting for login screen");
}
Future<bool> acknowledgeNewServerVersion() async {
final result = find.text("Acknowledge");
if (!tester.any(result)) {
return false;
}
await tester.tap(result);
await tester.pump();
return true;
}
Future<void> enterCredentials({
String server = "",
String email = "",
String password = "",
}) async {
final loginForms = find.byType(TextFormField);
await tester.pump(const Duration(milliseconds: 500));
await tester.enterText(loginForms.at(0), email);
await tester.pump(const Duration(milliseconds: 500));
await tester.enterText(loginForms.at(1), password);
await tester.pump(const Duration(milliseconds: 500));
await tester.enterText(loginForms.at(2), server);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
}
Future<void> enterCredentialsOf(LoginCredentials credentials) async {
await enterCredentials(
server: credentials.server,
email: credentials.email,
password: credentials.password,
);
}
Future<void> pressLoginButton() async {
final button = find.textContaining("login_form_button_text".tr());
await tester.tap(button);
}
Future<void> assertLoginSuccess({int timeoutSeconds = 15}) async {
for (var i = 0; i < timeoutSeconds * 2; i++) {
if (tester.any(find.text("home_page_building_timeline".tr()))) {
return;
}
await tester.pump(const Duration(milliseconds: 500));
}
fail("Login failed.");
}
Future<void> assertLoginFailed({int timeoutSeconds = 15}) async {
for (var i = 0; i < timeoutSeconds * 2; i++) {
if (tester.any(find.text("login_form_failed_login".tr()))) {
return;
}
await tester.pump(const Duration(milliseconds: 500));
}
fail("Timeout.");
}
}
enum LoginCredentials {
testInstance(
"https://flutter-int-test.preview.immich.app",
"demo@immich.app",
"demo",
),
testInstanceButWithWrongPassword(
"https://flutter-int-test.preview.immich.app",
"demo@immich.app",
"wrong",
),
wrongInstanceUrl(
"https://does-not-exist.preview.immich.app",
"demo@immich.app",
"demo",
);
const LoginCredentials(this.server, this.email, this.password);
final String server;
final String email;
final String password;
}

View File

@@ -13,6 +13,8 @@ PODS:
- FMDB/standard (2.7.5)
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_ios (0.0.1):
@@ -42,6 +44,7 @@ DEPENDENCIES:
- flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
@@ -69,6 +72,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios:
@@ -95,6 +100,7 @@ SPEC CHECKSUMS:
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604

View File

@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 82;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 82;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 82;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.42.0</string>
<string>1.43.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>79</string>
<string>82</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>

View File

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

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000301">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000396">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.73906">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.478301">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.857767">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.846552">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.648708">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.367554">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="88.88212">
<testcase classname="fastlane.lanes" name="4: build_app" time="75.618447">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="162.957763">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="47.502114">
</testcase>

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -29,12 +28,11 @@ import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'constants/hive_box.dart';
void main() async {
await Hive.initFlutter();
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
Hive.registerAdapter(ImmichLoggerMessageAdapter());
await initApp();
runApp(getMainWidget());
}
Future<void> openBoxes() async {
await Future.wait([
Hive.openBox<ImmichLoggerMessage>(immichLoggerBox),
Hive.openBox(userInfoBox),
@@ -47,12 +45,16 @@ void main() async {
if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
EasyLocalization.ensureInitialized(),
]);
}
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
),
);
Future<void> initApp() async {
await Hive.initFlutter();
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
Hive.registerAdapter(ImmichLoggerMessageAdapter());
await openBoxes();
if (kReleaseMode && Platform.isAndroid) {
try {
@@ -65,15 +67,15 @@ void main() async {
// Initialize Immich Logger Service
ImmichLogger().init();
}
runApp(
EasyLocalization(
supportedLocales: locales,
path: translationsPath,
useFallbackTranslations: true,
fallbackLocale: locales.first,
child: const ProviderScope(child: ImmichApp()),
),
Widget getMainWidget() {
return EasyLocalization(
supportedLocales: locales,
path: translationsPath,
useFallbackTranslations: true,
fallbackLocale: locales.first,
child: const ProviderScope(child: ImmichApp()),
);
}

View File

@@ -6,10 +6,10 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:openapi/api.dart';
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService)
SharedAlbumNotifier(this._albumService, this._sharedAlbumCacheService)
: super([]);
final AlbumService _sharedAlbumService;
final AlbumService _albumService;
final SharedAlbumCacheService _sharedAlbumCacheService;
_cacheState() {
@@ -22,7 +22,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
List<String> sharedUserIds,
) async {
try {
var newAlbum = await _sharedAlbumService.createAlbum(
var newAlbum = await _albumService.createAlbum(
albumName,
assets,
sharedUserIds,
@@ -47,7 +47,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
}
List<AlbumResponseDto>? sharedAlbums =
await _sharedAlbumService.getAlbums(isShared: true);
await _albumService.getAlbums(isShared: true);
if (sharedAlbums != null) {
state = sharedAlbums;
@@ -61,7 +61,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
}
Future<bool> leaveAlbum(String albumId) async {
var res = await _sharedAlbumService.leaveAlbum(albumId);
var res = await _albumService.leaveAlbum(albumId);
if (res) {
state = state.where((album) => album.id != albumId).toList();
@@ -76,7 +76,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
String albumId,
List<String> assetIds,
) async {
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
var res = await _albumService.removeAssetFromAlbum(albumId, assetIds);
if (res) {
return true;

View File

@@ -0,0 +1,132 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:openapi/api.dart';
class AddToAlbumBottomSheet extends HookConsumerWidget {
/// The asset to add to an album
final List<Asset> assets;
const AddToAlbumBottomSheet({
Key? key,
required this.assets,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider);
final albumService = ref.watch(albumServiceProvider);
final sharedAlbums = ref.watch(sharedAlbumProvider);
useEffect(
() {
// Fetch album updates, e.g., cover image
ref.read(albumProvider.notifier).getAllAlbums();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return null;
},
[],
);
void addToAlbum(AlbumResponseDto album) async {
final result = await albumService.addAdditionalAssetToAlbum(
assets,
album.id,
);
if (result != null) {
if (result.alreadyInAlbum.isNotEmpty) {
ImmichToast.show(
context: context,
msg: 'Already in ${album.albumName}',
);
} else {
ImmichToast.show(
context: context,
msg: 'Added to ${album.albumName}',
);
}
}
ref.read(albumProvider.notifier).getAllAlbums();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
Navigator.pop(context);
}
return Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Align(
alignment: Alignment.center,
child: CustomDraggingHandle(),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Add to album',
style: Theme.of(context).textTheme.headline2,
),
TextButton.icon(
icon: const Icon(Icons.add),
label: const Text('Create new album'),
onPressed: () {
ref
.watch(assetSelectionProvider.notifier)
.removeAll();
ref
.watch(assetSelectionProvider.notifier)
.addNewAssets(assets);
AutoRouter.of(context).push(
CreateAlbumRoute(
isSharedAlbum: false,
initialAssets: assets,
),
);
},
),
],
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: AddToAlbumSliverList(
albums: albums,
sharedAlbums: sharedAlbums,
onAddToAlbum: addToAlbum,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart';
import 'package:openapi/api.dart';
class AddToAlbumSliverList extends HookConsumerWidget {
/// The asset to add to an album
final List<AlbumResponseDto> albums;
final List<AlbumResponseDto> sharedAlbums;
final void Function(AlbumResponseDto) onAddToAlbum;
const AddToAlbumSliverList({
Key? key,
required this.onAddToAlbum,
required this.albums,
required this.sharedAlbums,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1),
(context, index) {
// Build shared expander
if (index == 0 && sharedAlbums.isNotEmpty) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ExpansionTile(
title: const Text('Shared'),
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
leading: const Icon(Icons.group),
children: sharedAlbums.map((album) =>
AlbumThumbnailListTile(
album: album,
onTap: () => onAddToAlbum(album),
),
).toList(),
),
);
}
// Build albums list
final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0);
final album = albums[offset];
return AlbumThumbnailListTile(
album: album,
onTap: () => onAddToAlbum(album),
);
}
),
);
}
}

View File

@@ -0,0 +1,116 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class AlbumThumbnailListTile extends StatelessWidget {
const AlbumThumbnailListTile({
Key? key,
required this.album,
this.onTap,
}) : super(key: key);
final AlbumResponseDto album;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
var cardSize = 68.0;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
buildEmptyThumbnail() {
return Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
child: SizedBox(
height: cardSize,
width: cardSize,
child: const Center(
child: Icon(Icons.no_photography),
),
),
);
}
buildAlbumThumbnail() {
return CachedNetworkImage(
width: cardSize,
height: cardSize,
fit: BoxFit.cover,
fadeInDuration: const Duration(milliseconds: 200),
imageUrl: getAlbumThumbnailUrl(
album,
type: ThumbnailFormat.JPEG,
),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
);
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap ??
() {
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
},
child: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: album.albumThumbnailAssetId == null
? buildEmptyThumbnail()
: buildAlbumThumbnail(),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
album.albumName,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
album.assetCount == 1
? 'album_thumbnail_card_item'
: 'album_thumbnail_card_items',
style: const TextStyle(
fontSize: 12,
),
).tr(args: ['${album.assetCount}']),
if (album.shared)
const Text(
'album_thumbnail_card_shared',
style: TextStyle(
fontSize: 12,
),
).tr()
],
)
],
),
),
],
),
),
);
}
}

View File

@@ -96,7 +96,8 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
if (isSuccess) {
Navigator.pop(context);
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
ref.refresh(sharedAlbumDetailProvider(albumId));
ref.watch(albumProvider.notifier).getAllAlbums();
ref.invalidate(sharedAlbumDetailProvider(albumId));
} else {
Navigator.pop(context);
ImmichToast.show(

View File

@@ -1,8 +1,11 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
@@ -62,7 +65,8 @@ class AlbumViewerPage extends HookConsumerWidget {
if (addAssetsResult != null &&
addAssetsResult.successfullyAdded > 0) {
ref.refresh(sharedAlbumDetailProvider(albumId));
ref.watch(albumProvider.notifier).getAllAlbums();
ref.invalidate(sharedAlbumDetailProvider(albumId));
}
ImmichLoadingOverlayController.appLoader.hide();
@@ -88,7 +92,7 @@ class AlbumViewerPage extends HookConsumerWidget {
.addAdditionalUserToAlbum(sharedUserIds, albumId);
if (isSuccess) {
ref.refresh(sharedAlbumDetailProvider(albumId));
ref.invalidate(sharedAlbumDetailProvider(albumId));
}
ImmichLoadingOverlayController.appLoader.hide();
@@ -246,32 +250,45 @@ class AlbumViewerPage extends HookConsumerWidget {
);
}
Future<bool> onWillPop() async {
final isMultiselectEnable = ref.read(assetSelectionProvider).selectedAssetsInAlbumViewer.isNotEmpty;
if (isMultiselectEnable) {
ref.watch(assetSelectionProvider.notifier).removeAll();
return false;
}
return true;
}
Widget buildBody(AlbumResponseDto albumInfo) {
return GestureDetector(
onTap: () {
titleFocusNode.unfocus();
},
child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
heightScrollThumb: 48.0,
child: CustomScrollView(
return WillPopScope(
onWillPop: onWillPop,
child: GestureDetector(
onTap: () {
titleFocusNode.unfocus();
},
child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
slivers: [
buildHeader(albumInfo),
SliverPersistentHeader(
pinned: true,
delegate: ImmichSliverPersistentAppBarDelegate(
minHeight: 50,
maxHeight: 50,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: buildControlButton(albumInfo),
heightScrollThumb: 48.0,
child: CustomScrollView(
controller: scrollController,
slivers: [
buildHeader(albumInfo),
SliverPersistentHeader(
pinned: true,
delegate: ImmichSliverPersistentAppBarDelegate(
minHeight: 50,
maxHeight: 50,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: buildControlButton(albumInfo),
),
),
),
),
buildImageGrid(albumInfo)
],
buildImageGrid(albumInfo)
],
),
),
),
);

View File

@@ -11,12 +11,18 @@ import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
// ignore: must_be_immutable
class CreateAlbumPage extends HookConsumerWidget {
bool isSharedAlbum;
final bool isSharedAlbum;
final List<Asset>? initialAssets;
CreateAlbumPage({Key? key, required this.isSharedAlbum}) : super(key: key);
const CreateAlbumPage({
Key? key,
required this.isSharedAlbum,
this.initialAssets,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {

View File

@@ -22,7 +22,7 @@ class LibraryPage extends HookConsumerWidget {
[],
);
Widget _buildAppBar() {
Widget buildAppBar() {
return const SliverAppBar(
centerTitle: true,
floating: true,
@@ -40,7 +40,7 @@ class LibraryPage extends HookConsumerWidget {
);
}
Widget _buildCreateAlbumButton() {
Widget buildCreateAlbumButton() {
return GestureDetector(
onTap: () {
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
@@ -83,7 +83,7 @@ class LibraryPage extends HookConsumerWidget {
return Scaffold(
body: CustomScrollView(
slivers: [
_buildAppBar(),
buildAppBar(),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(12.0),
@@ -99,7 +99,7 @@ class LibraryPage extends HookConsumerWidget {
child: Wrap(
spacing: 12,
children: [
_buildCreateAlbumButton(),
buildCreateAlbumButton(),
for (var album in albums)
AlbumThumbnailCard(
album: album,

View File

@@ -188,7 +188,7 @@ class ExifBottomSheet extends HookConsumerWidget {
),
),
subtitle: Text(
"ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ",
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ",
),
),
],

View File

@@ -1,205 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
import 'package:photo_manager/photo_manager.dart'
show AssetEntityImageProvider, ThumbnailSize;
import 'package:photo_view/photo_view.dart';
enum _RemoteImageStatus { empty, thumbnail, preview, full }
class _RemotePhotoViewState extends State<RemotePhotoView> {
late ImageProvider _imageProvider;
_RemoteImageStatus _status = _RemoteImageStatus.empty;
bool _zoomedIn = false;
late ImageProvider _fullProvider;
late ImageProvider _previewProvider;
late ImageProvider _thumbnailProvider;
@override
Widget build(BuildContext context) {
final bool forbidZoom = _status == _RemoteImageStatus.thumbnail;
return IgnorePointer(
ignoring: forbidZoom,
child: Listener(
onPointerMove: handleSwipUpDown,
child: PhotoView(
imageProvider: _imageProvider,
minScale: PhotoViewComputedScale.contained,
enablePanAlways: false,
scaleStateChangedCallback: _scaleStateChanged,
),
),
);
}
void handleSwipUpDown(PointerMoveEvent details) {
int sensitivity = 15;
if (_zoomedIn) {
return;
}
if (details.delta.dy > sensitivity) {
widget.onSwipeDown();
} else if (details.delta.dy < -sensitivity) {
widget.onSwipeUp();
}
}
void _scaleStateChanged(PhotoViewScaleState state) {
_zoomedIn = state != PhotoViewScaleState.initial;
if (_zoomedIn) {
widget.isZoomedListener.value = true;
} else {
widget.isZoomedListener.value = false;
}
widget.isZoomedFunction();
}
CachedNetworkImageProvider _authorizedImageProvider(
String url,
String cacheKey,
) {
return CachedNetworkImageProvider(
url,
headers: {"Authorization": widget.authToken},
cacheKey: cacheKey,
);
}
void _performStateTransition(
_RemoteImageStatus newStatus,
ImageProvider provider,
) {
if (_status == newStatus) return;
if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.preview &&
newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.preview) return;
if (!mounted) return;
setState(() {
_status = newStatus;
_imageProvider = provider;
});
}
void _loadImages() {
if (widget.asset.isLocal) {
_imageProvider = AssetEntityImageProvider(
widget.asset.local!,
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(250),
);
_fullProvider = AssetEntityImageProvider(widget.asset.local!);
_fullProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo image, _) {
_performStateTransition(
_RemoteImageStatus.full,
_fullProvider,
);
}),
);
return;
}
_thumbnailProvider = _authorizedImageProvider(
getThumbnailUrl(widget.asset.remote!),
getThumbnailCacheKey(widget.asset.remote!),
);
_imageProvider = _thumbnailProvider;
_thumbnailProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(
_RemoteImageStatus.thumbnail,
_thumbnailProvider,
);
}),
);
if (widget.loadPreview) {
_previewProvider = _authorizedImageProvider(
getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG),
getThumbnailCacheKey(widget.asset.remote!, type: ThumbnailFormat.JPEG),
);
_previewProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.preview, _previewProvider);
}),
);
}
if (widget.loadOriginal) {
_fullProvider = _authorizedImageProvider(
getImageUrl(widget.asset.remote!),
getImageCacheKey(widget.asset.remote!),
);
_fullProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.full, _fullProvider);
}),
);
}
}
@override
void initState() {
super.initState();
_loadImages();
}
@override
void dispose() async {
super.dispose();
if (_status == _RemoteImageStatus.full) {
await _fullProvider.evict();
} else if (_status == _RemoteImageStatus.preview) {
await _previewProvider.evict();
} else if (_status == _RemoteImageStatus.thumbnail) {
await _thumbnailProvider.evict();
}
await _imageProvider.evict();
}
}
class RemotePhotoView extends StatefulWidget {
const RemotePhotoView({
Key? key,
required this.asset,
required this.authToken,
required this.loadPreview,
required this.loadOriginal,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onSwipeDown,
required this.onSwipeUp,
}) : super(key: key);
final Asset asset;
final String authToken;
final bool loadPreview;
final bool loadOriginal;
final void Function() onSwipeDown;
final void Function() onSwipeUp;
final void Function() isZoomedFunction;
final ValueNotifier<bool> isZoomedListener;
@override
State<StatefulWidget> createState() {
return _RemotePhotoViewState();
}
}

View File

@@ -11,6 +11,7 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
required this.onDownloadPressed,
required this.onSharePressed,
required this.onDeletePressed,
required this.onAddToAlbumPressed,
required this.onToggleMotionVideo,
required this.isPlayingMotionVideo,
}) : super(key: key);
@@ -20,6 +21,7 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final VoidCallback onDeletePressed;
final VoidCallback onAddToAlbumPressed;
final Function onSharePressed;
final bool isPlayingMotionVideo;
@@ -80,6 +82,18 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
color: Colors.grey[200],
),
),
if (asset.isRemote)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onAddToAlbumPressed();
},
icon: Icon(
Icons.add,
color: Colors.grey[200],
),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,

View File

@@ -1,21 +1,32 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:openapi/api.dart' as api;
// ignore: must_be_immutable
class GalleryViewerPage extends HookConsumerWidget {
@@ -39,7 +50,8 @@ class GalleryViewerPage extends HookConsumerWidget {
final isZoomed = useState<bool>(false);
final indexOfAsset = useState(assetList.indexOf(asset));
final isPlayingMotionVideo = useState(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
late Offset localPosition;
final authToken = 'Bearer ${box.get(accessTokenKey)}';
PageController controller =
PageController(initialPage: assetList.indexOf(asset));
@@ -56,7 +68,7 @@ class GalleryViewerPage extends HookConsumerWidget {
[],
);
getAssetExif() async {
void getAssetExif() async {
if (assetList[indexOfAsset.value].isRemote) {
assetDetail = await ref
.watch(assetServiceProvider)
@@ -67,27 +79,96 @@ class GalleryViewerPage extends HookConsumerWidget {
}
}
void showInfo() {
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
/// Thumbnail image of a remote asset. Required asset.remote != null
ImageProvider remoteThumbnailImageProvider(Asset asset, api.ThumbnailFormat type) {
return CachedNetworkImageProvider(
getThumbnailUrl(
asset.remote!,
type: type,
),
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
cacheKey: getThumbnailCacheKey(
asset.remote!,
type: type,
),
headers: {"Authorization": authToken},
);
}
//make isZoomed listener call instead
void isZoomedMethod() {
if (isZoomedListener.value) {
isZoomed.value = true;
} else {
isZoomed.value = false;
/// Original (large) image of a remote asset. Required asset.remote != null
ImageProvider originalImageProvider(Asset asset) {
return CachedNetworkImageProvider(
getImageUrl(asset.remote!),
cacheKey: getImageCacheKey(asset.remote!),
headers: {"Authorization": authToken},
);
}
/// Thumbnail image of a local asset. Required asset.local != null
ImageProvider localThumbnailImageProvider(Asset asset) {
return AssetEntityImageProvider(
asset.local!,
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(250),
);
}
/// Original (large) image of a local asset. Required asset.local != null
ImageProvider localImageProvider(Asset asset) {
return AssetEntityImageProvider(asset.local!);
}
void precacheNextImage(int index) {
if (index < assetList.length && index > 0) {
final asset = assetList[index];
if (asset.isLocal) {
// Preload the local asset
precacheImage(localImageProvider(asset), context);
} else {
// Probably load WEBP either way
precacheImage(
remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.WEBP,
),
context,
);
if (isLoadPreview.value) {
// Precache the JPEG thumbnail
precacheImage(
remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.JPEG,
),
context,
);
}
if (isLoadOriginal.value) {
// Preload the original asset
precacheImage(
originalImageProvider(asset),
context,
);
}
}
}
}
void showInfo() {
if (assetList[indexOfAsset.value].isRemote) {
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
}
@@ -105,6 +186,44 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
void addToAlbum(Asset addToAlbumAsset) {
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
context: context,
builder: (BuildContext _) {
return AddToAlbumBottomSheet(
assets: [addToAlbumAsset],
);
},
);
}
void handleSwipeUpDown(DragUpdateDetails details) {
int sensitivity = 15;
int dxThreshhold = 50;
if (isZoomed.value) {
return;
}
// Check for delta from initial down point
final d = details.localPosition - localPosition;
// If the magnitude of the dx swipe is large, we probably didn't mean to go down
if (d.dx.abs() > dxThreshhold) {
return;
}
if (details.delta.dy > sensitivity) {
AutoRouter.of(context).pop();
} else if (details.delta.dy < -sensitivity) {
showInfo();
}
}
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
@@ -130,63 +249,96 @@ class GalleryViewerPage extends HookConsumerWidget {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}),
onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
),
body: SafeArea(
child: PageView.builder(
controller: controller,
pageSnapping: true,
physics: isZoomed.value
? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(),
child: PhotoViewGallery.builder(
scaleStateChangedCallback: (state) => isZoomed.value = state != PhotoViewScaleState.initial,
pageController: controller,
scrollPhysics: isZoomed.value
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
: (Platform.isIOS
? const BouncingScrollPhysics() // Use bouncing physics for iOS
: const ClampingScrollPhysics() // Use heavy physics for Android
),
itemCount: assetList.length,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
// Precache image
if (indexOfAsset.value < value) {
// Moving forwards, so precache the next asset
precacheNextImage(value + 1);
} else {
// Moving backwards, so precache previous asset
precacheNextImage(value - 1);
}
indexOfAsset.value = value;
HapticFeedback.selectionClick();
},
itemBuilder: (context, index) {
getAssetExif();
loadingBuilder: isLoadPreview.value ? (context, event) {
final asset = assetList[indexOfAsset.value];
if (!asset.isLocal) {
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
// Three-Stage Loading (WEBP -> JPEG -> Original)
final webPThumbnail = CachedNetworkImage(
imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.WEBP),
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.WEBP),
httpHeaders: { 'Authorization': authToken },
progressIndicatorBuilder: (_, __, ___) => const Center(child: ImmichLoadingIndicator(),),
fit: BoxFit.contain,
);
if (assetList[index].isImage) {
if (isPlayingMotionVideo.value) {
return VideoViewerPage(
asset: assetList[index],
isMotionVideo: true,
onVideoEnded: () {
isPlayingMotionVideo.value = false;
},
);
} else {
return ImageViewerPage(
authToken: 'Bearer ${box.get(accessTokenKey)}',
isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener,
asset: assetList[index],
heroTag: assetList[index].id,
loadPreview: isLoadPreview.value,
loadOriginal: isLoadOriginal.value,
showExifSheet: showInfo,
);
}
return CachedNetworkImage(
imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.JPEG),
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.JPEG),
httpHeaders: { 'Authorization': authToken },
fit: BoxFit.contain,
placeholder: (_, __) => webPThumbnail,
);
} else {
return GestureDetector(
onVerticalDragUpdate: (details) {
const int sensitivity = 15;
if (details.delta.dy > sensitivity) {
// swipe down
AutoRouter.of(context).pop();
} else if (details.delta.dy < -sensitivity) {
// swipe up
showInfo();
}
},
child: Hero(
tag: assetList[index].id,
child: VideoViewerPage(
asset: assetList[index],
isMotionVideo: false,
onVideoEnded: () {},
),
return Image(
image: localThumbnailImageProvider(asset),
fit: BoxFit.contain,
);
}
} : null,
builder: (context, index) {
getAssetExif();
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
// Show photo
final ImageProvider provider;
if (assetList[index].isLocal) {
provider = localImageProvider(assetList[index]);
} else {
if (isLoadOriginal.value) {
provider = originalImageProvider(assetList[index]);
} else {
provider = remoteThumbnailImageProvider(
assetList[index],
api.ThumbnailFormat.JPEG,
);
}
}
return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) => localPosition = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
imageProvider: provider,
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
minScale: PhotoViewComputedScale.contained,
);
} else {
return PhotoViewGalleryPageOptions.customChild(
onDragStart: (_, details, __) => localPosition = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
child: VideoViewerPage(
asset: assetList[index],
isMotionVideo: isPlayingMotionVideo.value,
onVideoEnded: () {
if (isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
),
);
}
@@ -196,3 +348,4 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
}

View File

@@ -1,84 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
// ignore: must_be_immutable
class ImageViewerPage extends HookConsumerWidget {
final String heroTag;
final Asset asset;
final String authToken;
final ValueNotifier<bool> isZoomedListener;
final void Function() isZoomedFunction;
final void Function()? showExifSheet;
final bool loadPreview;
final bool loadOriginal;
ImageViewerPage({
Key? key,
required this.heroTag,
required this.asset,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.loadPreview,
required this.loadOriginal,
this.showExifSheet,
}) : super(key: key);
Asset? assetDetail;
@override
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus;
getAssetExif() async {
if (asset.isRemote) {
assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id);
} else {
// TODO local exif parsing?
assetDetail = asset;
}
}
useEffect(
() {
getAssetExif();
return null;
},
[],
);
return Stack(
children: [
Center(
child: Hero(
tag: heroTag,
child: RemotePhotoView(
asset: asset,
authToken: authToken,
loadPreview: loadPreview,
loadOriginal: loadOriginal,
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener,
onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: (asset.isRemote && showExifSheet != null) ? showExifSheet! : () {},
),
),
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: ImmichLoadingIndicator(),
),
],
);
}
}

View File

@@ -357,7 +357,6 @@ class BackgroundService {
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
]);
ApiService apiService = ApiService();
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
BackupService backupService = BackupService(apiService);
AppSettingsService settingsService = AppSettingsService();

View File

@@ -29,6 +29,7 @@ final backupServiceProvider = Provider(
);
class BackupService {
final httpClient = http.Client();
final ApiService _apiService;
BackupService(this._apiService);
@@ -274,13 +275,16 @@ class BackupService {
setCurrentUploadAssetCb(
CurrentUploadAsset(
id: entity.id,
createdAt: entity.createDateTime,
createdAt: entity.createDateTime.year == 1970
? entity.modifiedDateTime
: entity.createDateTime,
fileName: originalFileName,
fileType: _getAssetType(entity.type),
),
);
var response = await req.send(cancellationToken: cancelToken);
var response =
await httpClient.send(req, cancellationToken: cancelToken);
if (response.statusCode == 200) {
// asset is a duplicate (already exists on the server)
@@ -332,7 +336,6 @@ class BackupService {
Future<MultipartFile?> _getLivePhotoFile(AssetEntity entity) async {
var motionFilePath = await entity.getMediaUrl();
// var motionFilePath = '/var/mobile/Media/DCIM/103APPLE/IMG_3371.MOV'
if (motionFilePath != null) {
var validPath = motionFilePath.replaceAll('file://', '');

View File

@@ -0,0 +1,219 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:photo_manager/photo_manager.dart';
class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
const CurrentUploadingAssetInfoBox({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
var asset = ref.watch(backupProvider).currentUploadAsset;
var uploadProgress = ref.watch(backupProvider).progressInPercentage;
final isShowThumbnail = useState(false);
String getAssetCreationDate() {
return DateFormat.yMMMMd('en_US').format(
DateTime.parse(
asset.createdAt.toString(),
).toLocal(),
);
}
Widget buildErrorChip() {
return ActionChip(
avatar: Icon(
Icons.info,
color: Colors.red[400],
),
elevation: 1,
visualDensity: VisualDensity.compact,
label: Text(
"backup_controller_page_failed",
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
fontSize: 11,
),
).tr(
args: [ref.watch(errorBackupListProvider).length.toString()],
),
backgroundColor: Colors.white,
onPressed: () {
AutoRouter.of(context).push(const FailedBackupStatusRoute());
},
);
}
Widget buildAssetInfoTable() {
return Table(
border: TableBorder.all(
color: Theme.of(context).primaryColorLight,
width: 1,
),
children: [
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[100],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
'backup_controller_page_filename',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
args: [asset.fileName, asset.fileType.toLowerCase()],
),
),
),
],
),
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[200],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
"backup_controller_page_created",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
args: [getAssetCreationDate()],
),
),
),
],
),
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[100],
),
children: [
TableCell(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
"backup_controller_page_id",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(args: [asset.id]),
),
),
],
),
],
);
}
buildAssetThumbnail() async {
var assetEntity = await AssetEntity.fromId(asset.id);
if (assetEntity != null) {
return assetEntity.thumbnailDataWithSize(
const ThumbnailSize(500, 500),
quality: 100,
);
}
}
return FutureBuilder<Uint8List?>(
future: buildAssetThumbnail(),
builder: (context, thumbnail) => ListTile(
leading: AnimatedCrossFade(
alignment: Alignment.centerLeft,
firstChild: GestureDetector(
onTap: () => isShowThumbnail.value = false,
child: thumbnail.hasData
? ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image.memory(
thumbnail.data!,
fit: BoxFit.cover,
width: 50,
height: 50,
),
)
: const SizedBox(
width: 50,
height: 50,
child: Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator.adaptive(
strokeWidth: 1,
),
),
),
),
secondChild: GestureDetector(
onTap: () => isShowThumbnail.value = true,
child: Icon(
Icons.image_outlined,
color: Theme.of(context).primaryColor,
size: 30,
),
),
crossFadeState: isShowThumbnail.value
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 200),
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"backup_controller_page_uploading_file_info",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(),
],
),
subtitle: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Expanded(
child: LinearProgressIndicator(
minHeight: 10.0,
value: uploadProgress / 100.0,
backgroundColor: Colors.grey,
color: Theme.of(context).primaryColor,
),
),
Text(
" ${uploadProgress.toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 12),
)
],
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: buildAssetInfoTable(),
),
],
),
),
);
}
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -467,155 +468,6 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
buildCurrentBackupAssetInfoCard() {
return ListTile(
leading: Icon(
Icons.info_outline_rounded,
color: Theme.of(context).primaryColor,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"backup_controller_page_uploading_file_info",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
if (ref.watch(errorBackupListProvider).isNotEmpty)
ActionChip(
avatar: Icon(
Icons.info,
color: Colors.red[400],
),
elevation: 1,
visualDensity: VisualDensity.compact,
label: Text(
"backup_controller_page_failed",
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
fontSize: 11,
),
).tr(
args: [ref.watch(errorBackupListProvider).length.toString()],
),
backgroundColor: Colors.white,
onPressed: () {
AutoRouter.of(context).push(const FailedBackupStatusRoute());
},
),
],
),
subtitle: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Expanded(
child: LinearProgressIndicator(
minHeight: 10.0,
value: backupState.progressInPercentage / 100.0,
backgroundColor: Colors.grey,
color: Theme.of(context).primaryColor,
),
),
Text(
" ${backupState.progressInPercentage.toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 12),
)
],
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Table(
border: TableBorder.all(
color: Theme.of(context).primaryColorLight,
width: 1,
),
children: [
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[100],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
'backup_controller_page_filename',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
args: [
backupState.currentUploadAsset.fileName,
backupState.currentUploadAsset.fileType
.toLowerCase()
],
),
),
),
],
),
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[200],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
"backup_controller_page_created",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
args: [
DateFormat.yMMMMd('en_US').format(
DateTime.parse(
backupState.currentUploadAsset.createdAt
.toString(),
).toLocal(),
)
],
),
),
),
],
),
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[100],
),
children: [
TableCell(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
"backup_controller_page_id",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(args: [backupState.currentUploadAsset.id]),
),
),
],
),
],
),
),
],
),
);
}
void startBackup() {
ref.watch(errorBackupListProvider.notifier).empty();
if (ref.watch(backupProvider).backupProgress !=
@@ -699,7 +551,7 @@ class BackupControllerPage extends HookConsumerWidget {
const Divider(),
buildStorageInformation(),
const Divider(),
buildCurrentBackupAssetInfoCard(),
const CurrentUploadingAssetInfoBox(),
Padding(
padding: const EdgeInsets.only(
top: 24,

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