Compare commits

...

166 Commits

Author SHA1 Message Date
Alex
83716ae1bc Added changelog note 2022-08-21 12:30:15 -07:00
Alex
5cd4d2d158 Added condition to show notification setting on android only 2022-08-21 11:04:01 -07:00
Alex
13bb6d469b Pump version for release 2022-08-21 09:56:52 -07:00
Matthias Rupp
8e4c4c34e4 Use CachedNetworkImage and separate cache for thumbnails on library page (#509)
* Use CachedNetworkImage and separate cache for thumbnails on library page

* Use caching for shared albums as well

* Introduce cache service
2022-08-21 09:41:36 -07:00
Fynn Petersen-Frey
3125d04f32 show notifications on background backup errors (#496)
* show notifications on background backup errors

* settings page to configure (background backup error) notifications

* persist time since failed background backup

* fix darkmode slider color
2022-08-21 09:29:24 -07:00
Alex
c436c57cc9 Fixed immich-machine-learning container not starting correctly in production 2022-08-20 23:04:10 -07:00
Thanh Pham
7f9f825589 fix(server): correct media info (#508)
* fix(server): correct media info

* fix(server): video metadata
2022-08-20 22:58:47 -07:00
Alex
da9aed5c11 Fixed e2e container stage 2022-08-20 22:37:55 -07:00
Alex
10ef3509dd Fixed machine-learning container cannot start prod 2022-08-20 22:27:25 -07:00
Alex
3dc538f9e6 Fixed machine-learning container cannot start prod 2022-08-20 22:26:47 -07:00
Thanh Pham
1e29ff322d build(server): minimal container (#506)
* build(server): update Dockerfile

* build(server): fix dockerfile

* build(machine-learning): multiple build stages

* build(server): update Dockerfile
2022-08-20 21:19:02 -07:00
Thanh Pham
9c30d58b10 feat(server): preserve caption fields and extract mediainfo for video (#505)
* feat(server): preserve caption fields and extract mediainfo for video

* Fixed Geocoding missing info leads to fail EXIF extraction for the whole file

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-08-20 16:31:37 -07:00
Matthias Rupp
013a0f8324 Customization options for asset grid (#498)
* Add settings options for number of assets per row and storage indicator

* Add attributes to enum to avoid duplicate code

* Also apply customizations to albums

* Minor Refactorings

* Three stage loading i18n fix
2022-08-20 14:19:40 -07:00
Alex
07b58f46f9 Merge branch 'main' of github.com:alextran1501/immich 2022-08-20 08:01:59 -07:00
Alex
566e118a19 Added pt-BR translation locale to mobile app 2022-08-20 08:01:52 -07:00
Oton
0e18c88534 pt-BR Translation: Translation into Portuguese Brazil. (#500) 2022-08-20 08:01:25 -07:00
Alex
068d06b9ee Add x-adobe-dng to support file type (#504) 2022-08-20 07:50:58 -07:00
Thanh Pham
0cf7606ec9 fix(server): remove albumThumbnailAssetId when album is empty (#495) 2022-08-19 11:47:14 -07:00
Alex
25338ce02f Fixed issue with filename is blank on iOS causing update to fail 2022-08-18 16:27:08 -05:00
Alex Tran
4805d86a7c pump version 2022-08-18 15:01:45 -05:00
Fynn Petersen-Frey
33b1410d82 upload new photos in background with a service (#382)
* properly done background backup service

* new concurrency/locking management with heartbeat

fix communication erros with Kotlin plugin on start/stop service methods

better error handling for BackgroundService public methods

Add default notification message when service is running

* configurable WiFi & charging requirement for service

* use translations in background service
2022-08-18 09:41:59 -05:00
r3nor
f35ebec7c6 Readme redistribution and reorganisation (#485)
* redistribute and organize readme

* Fix typos

* Change developers to development

* Remove mobile app emoji

* Multiple changes

- Removed the .env example inbody to link to the .env.example file; this way it won't be needed to change the readme if the .env file changes and the readme gets shorter.
- Redistributed the steps:
  - Inline instead of tables
  - bullets in steps
- Fix wording
2022-08-18 08:31:00 -05:00
Thanh Pham
3aa6ee0320 feat: remove webp on asset deleted as well (#489)
* fix(server): remove webp file on asset deleted

* chore(server): job not fail when file not found
2022-08-18 08:25:03 -05:00
Thanh Pham
cdb0aa00d8 feat(server, microservices): add bull prefix (#490) 2022-08-18 08:24:07 -05:00
Alex
9de7b8d3a7 Create github-repo-stats.yml 2022-08-16 22:56:20 -05:00
Max
4a28a46612 fix spelling mistakes (#479) 2022-08-16 13:50:45 -05:00
Malte Kiefer
16561d15ff Added German translation for the settings view (#478)
* added translation for the settings view

* added translation for the settings view
2022-08-16 11:00:48 -05:00
Alex
9642ad2820 Fixed Websocket not getting correct data on mobile 2022-08-15 23:43:12 -05:00
Alex
e2169a26c2 Remove padding bottom on photos page mobile 2022-08-15 23:11:54 -05:00
Alex
f697922f32 Remove hardcode UIsystemLight in info.plist (#475) 2022-08-15 23:10:51 -05:00
Alex Tran
1390d01763 Up version 2022-08-15 19:13:51 -05:00
Alex
86f780871c Fixed different lettercases in email create different user (#470)
* Fixed different lettercases in email create different user

* Fixed test
2022-08-15 19:11:08 -05:00
Alex
c1b22125fd Add mobile dark mode and user setting (#468)
* styling light and dark theme

* Icon topbar

* Fixed app bar title dark theme

* Fixed issue with getting thumbnail for things

* Refactor sharing page

* Refactor scroll thumb

* Refactor chip in auto  backup indiation button

* Refactor sharing page

* Added theme toggle

* Up version for testflight build

* Refactor backup controller page

* Refactor album selection page

* refactor album pages

* Refactor gradient color profile header

* Added theme switcher

* Register app theme correctly

* Added locale to the app

* Added translation key

* Styling for bottomsheet colors

* up server version

* Fixed font size

* Fixed overlapsed sliverappbar on photos screen
2022-08-15 18:53:30 -05:00
Alex
30f069a5db Add settings screen on mobile (#463)
* Refactor profile drawer to sub component

* Added setting page, routing with some options

* Added setting service

* Implement three stage settings

* get app setting for three stage loading
2022-08-13 15:51:09 -05:00
bo0tzz
2bf6cd9241 Fix redirect to login page after password change (#461)
* Fix redirect to login page after password change

Copied from the similar fix in #414

* Fix typo in change-password form

* Remove misplaced text from user management page
2022-08-13 09:54:29 -05:00
Alex Tran
87d2a954a3 Fixed error handling with catch block 2022-08-12 22:29:24 -05:00
Alex
a388c5a642 Fixed webp upload on web (#460) 2022-08-12 21:52:30 -05:00
Alex Tran
4b34f017ca cosmetic change 2022-08-12 21:19:54 -05:00
Alex Tran
5c1d1dd5a1 Added version note for f-droid 2022-08-12 20:10:00 -05:00
Alex Tran
1580d27c23 Up version 2022-08-12 20:06:45 -05:00
Alex
4b9187928c Edit user on the web (#458)
* Added dispatch event for edit user

* Fixed import location

* solve merge conflict

* Fixed issue not admin user can access admin page

* Implemented edit user and password reset
2022-08-12 14:25:19 -05:00
Alex Tran
5b7236f6ad Temporary remove bug tests 2022-08-11 23:17:09 -05:00
Alex Tran
6fb439b580 Fixed merge conflict 2022-08-11 13:46:42 -05:00
Alex Tran
a8334b5c27 Fixed test again 2022-08-11 13:46:11 -05:00
Alex Tran
e1cac93945 Fixed test 2022-08-11 09:29:53 -05:00
R0GGER
081f9f5bce typo (#456) 2022-08-11 08:33:44 -05:00
Alex Tran
25ccc5660d Merge branch 'main' of github.com:immich-app/immich 2022-08-11 08:27:48 -05:00
Alex Tran
b6d3e578f2 Added test and github action for unit tests 2022-08-11 08:27:44 -05:00
Matthias Rupp
52377c2dcf Fix sharing on iPad (#453) 2022-08-11 08:13:33 -05:00
Alex
5c78f707fe Modify Album API endpoint to return a count attribute instead of a full assets array (#454)
* Change API to return assets count and change web behavior accordingly

* Refactor assets.length

* Explicitly declare type of assetCount so Dart SDK understand it

* Finished refactoring on mobile
2022-08-10 22:48:25 -05:00
Alex Tran
bd5ed1b684 Merge branch 'main' of github.com:immich-app/immich 2022-08-09 19:12:32 -05:00
Alex Tran
e89339b813 Up server version 2022-08-09 19:12:21 -05:00
Alex
0b69feda40 Fixed checkbox render performance (#448) 2022-08-09 19:10:55 -05:00
Alex
339f7f776f Fixed setting high refresh rate crash ios release build 2022-08-08 23:43:48 -05:00
Alex Tran
7e6ccbad21 Up server version 2022-08-08 22:55:35 -05:00
Alex Tran
aac53e5cdc Up version for release 2022-08-08 22:39:32 -05:00
Alex Tran
cbec75a175 Rewording delete caution message 2022-08-08 22:13:36 -05:00
Alex
bf04d9eb39 Feature - Delete asset on the web (#436)
* Added selection mechanism to photos page

* Added control app bar

* Refactor AlbumAppBar into ControlAppBar

* Added addtional micro interactions when in multi selection mode

* Implemented delete selected asset and rerender
2022-08-08 22:06:11 -05:00
Malte Kiefer
3058c894b1 updated German translation (#444) 2022-08-08 21:21:02 -05:00
Matthias Rupp
e57e279fe1 Share assets from mobile to other apps (#435)
* Share unique assets

* Style share preparing dialog

* Share assets from multiselect

* Fix i18n

* Use navigator like in delete dialog

* Center bottom-bar buttons
2022-08-08 10:46:12 -05:00
dependabot[bot]
f43c58fc6d Bump docker/build-push-action from 3.1.0 to 3.1.1 (#441)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.1.0...v3.1.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-08 08:22:14 -05:00
Matthias Rupp
dea304ac39 Fix/album title (#440)
* Fix album title overflow

* i18n

* More i18n
2022-08-08 08:11:56 -05:00
Matthias Rupp
b46e834220 Mobile performance improvements (#417)
* First performance tweaks (caching and rendering improvemetns)

* Revert asset response caching

* 3-step image loading in asset viewer

* Prevent panning and zooming until full-scale version is loaded

* Loading indicator

* Adapt to gallery PR

* Cleanup

* Dart format

* Fix exif sheet

* Disable three stage loading until settings are available
2022-08-07 19:43:09 -05:00
Alex Tran
46f4905259 Up server version 2022-08-07 18:42:21 -05:00
Alex
28c7736ecd Fix error in logout procedure and guard for each route (#439) 2022-08-07 18:36:34 -05:00
Alex Tran
f881981c44 Fix typo in Readme 2022-08-07 08:22:03 -05:00
Alex
953d18e795 Remove serverEndpoint completely and fix upload path (#434) 2022-08-07 08:12:31 -05:00
Alex
b45024a97e Update README.md 2022-08-07 00:17:12 -05:00
Alex Tran
3dcdfa0166 Up android build version 2022-08-06 23:45:31 -05:00
Alex
2079583866 Update installation method and documentation (#424)
* Add installation script

* Populate instsall.sh

* format

* Get IP address on both macos and linux

* Update mobile version

* Remove test folder

* Added sed command for ios

* Added sed command for ios

* Fixed ios command

* Fixed ios command

* Added friendly debug message

* Update README

* Update Readme with new installation instruction

* Update message on instsallation script
2022-08-06 23:42:50 -05:00
Alex
b68358766b Remove VITE_SERVER_ENDPOINT dependency (#428)
* Move backend api to its own instance

* Remove external fetch hook

* Added endpoint for album

* Added endpoint for admin page

* Make request directly to immich-server

* Refactor unsued code
2022-08-06 18:14:54 -05:00
Alex Tran
cf2b9eddfa Pump version 1.20 2022-08-03 15:43:42 -05:00
Stevenson Chittumuri
8c184dc4d4 Enable swiping between assets (#381)
Enable swiping between assets (#381)

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Malte Kiefer <59220985+MalteKiefer@users.noreply.github.com>
Co-authored-by: Matthias Rupp <matthias.rupp@posteo.de>
2022-08-03 15:36:12 -05:00
Alex
e8d1f89a47 Implement album feature on mobile (#420)
* Refactor sharing to album

* Added library page in the bottom navigation bar

* Refactor SharedAlbumService to album service

* Refactor apiProvider to its file

* Added image grid

* render album thumbnail

* Using the wrap to render thumbnail and album info better

* Navigate to album viewer

* After deletion, navigate to the respective page of the shared and non-shared album

* Correctly remove album in local state

* Refactor create album page

* Implemented create non-shared album
2022-08-03 00:04:34 -05:00
Alex
0e85b0fd8f Remove print statement 2022-07-31 22:26:09 -05:00
Alex Tran
f7dc916e80 Fixed problem with Recent (isAll) album is both in exclude and include album list at the same time 2022-07-31 21:56:41 -05:00
Alex
03e7a254a2 Fixed logging out not redirect correctly in reverse proxy (#414)
* Remove check due to logout always success

* Added console log

* Remove console.log

* Up server version
2022-07-31 16:53:07 -05:00
Matthias Rupp
0ac9fe5a54 Load low- and high quality thumbnail in the same img tag to avoid flickering (#413) 2022-07-31 15:56:03 -05:00
Malte Kiefer
dc61fd925f fixed some German translations (#399) 2022-07-30 07:41:39 -05:00
Alex Tran
2aea08726f Update donation info 2022-07-29 13:42:39 -05:00
Alex Tran
746bec908b Update donation info 2022-07-29 13:41:29 -05:00
Alex Tran
8102e3b3f5 Fixed github action to conform with the move to org 2022-07-29 12:54:40 -05:00
Alex Tran
1ba998aa68 Added changlog for Fdroid release 2022-07-27 13:17:36 -05:00
Alex Tran
2de34f70ce Update readme 2022-07-27 13:09:52 -05:00
Alex Tran
8b9fd67d6f Remove AxiosError import due to production build error 2022-07-27 13:01:49 -05:00
Alex
97238a1621 Up version for release 2022-07-27 11:39:19 -05:00
Alex
ef4136d327 [WEB] Select album thumbnail (#383)
* Added context menu for album opionts

* choose asset for album thumbnail

* Refactor UpdateAlbumDto to accept albumThumbnailAssetId

* implemented changing album cover on web

* Fixed api change on mobile app
2022-07-27 11:16:02 -05:00
Alex
6dbca8d478 Added Japanese/Polish/Finish and fix Italian/Spanish translation 2022-07-27 11:14:21 -05:00
Alex
a305db9e6f [Localizely] Translations update (#384) 2022-07-27 11:07:37 -05:00
Alex Tran
59c1ea3097 Added 2-stage loading for album's thumbnail 2022-07-26 22:06:06 -05:00
Alex
03457f5d32 [WEB] Upload asset directly to album (#379)
* Added stores to get album assetId

* Upload assets and add to album

* Added comments

* resolve conflict when add assets from upload directly

* Filtered out duplicate asset before adding to the album
2022-07-26 20:53:25 -05:00
Alex
2336a6159c [WEB] Load thumbnail with native source property for faster load time (#378) 2022-07-26 15:13:08 -05:00
Alex Tran
e4c4b53fcd Added imageName as searchable text on database 2022-07-26 13:43:12 -05:00
Alex
83cbf51704 Use cookies for client requests (#377)
* Use cookie for frontend request

* Remove api helper to use SDK

* Added error handling to status box

* Remove additional places that check for session.user

* Refactor sending password

* prettier clean up

* remove deadcode

* Move all authentication requests to the client

* refactor upload panel to only fetch assets after the upload panel disappear

* Added keydown to remove focus on title change on album viewer
2022-07-26 12:28:07 -05:00
Alex Tran
2ebb755f00 Fixed corner radius for message board 2022-07-24 23:45:05 -05:00
Alex Tran
ec1c3a86f5 Added messages when there is no album or shared album 2022-07-24 23:30:30 -05:00
Alex
969f770df0 Delete album on web (#373)
* Show context menu

* Show context menu at the correct location

* Implement delete album button

* Delete album within album viewer
2022-07-24 22:47:12 -05:00
Alex Tran
9c3f848fa8 Use Webp for album thumbnail 2022-07-24 08:51:00 -05:00
Alex Tran
1ea6425cd1 Handle unhandled promises that lead to unable to login 2022-07-24 08:41:06 -05:00
Alex
052db5d748 Remove/Add asset in ablum on web (#371)
* Added interaction to select multiple thumbnail

* Fixed stutter transition

* Return AlbumResponseDto after removing an asset from album

* Render correctly when an array of thumbnail is updated

* Fixed wording

* Added native dialog for removing users from album

* Fixed rendering incorrect profile image on share user select dialog
2022-07-23 23:23:14 -05:00
bo0tzz
a35460cb84 Bump tfjs version to 3.19.0 for arm64 support (#368)
* Add linux/arm64 to machine-learning container build

* Bump tfjs version to 3.19.0

* Fix tfjs dependency error
2022-07-23 14:15:55 -05:00
Alex
ae93bbe2a7 Docker login only with branch from the repository (#370) 2022-07-23 13:48:53 -05:00
Alex
3b97c7729b Implement mechanism to remove and add shared user in album on web (#369)
* AFixed overlay issue of modal

* Added modal with existing user

* Added custom scrollbar to all pages

* Fixed Document is not define when access document DOM node in browswer

* Added context menu

* Added api to remove user from album

* Handle user leave album

* Added share button to non-shared album

* Added padding to album viewer:

* Fixed margin top of asset selection page

* Fixed issue cannot push to dockerhub
2022-07-23 13:08:49 -05:00
bo0tzz
6021124688 Move docker login step to after build (#367) 2022-07-23 11:05:13 -05:00
Alex
1d34976dd0 Implement album creation on web (#365)
* Added album creation button functionality

* Added input for album title

* Added select photos button

* Added page to select assets

* Show photo selection timeline

* Implemented update album name mechanism:

* Added selection mechanism

* Added selection mechanism with existing assets in album

* Refactored and added comments

* Refactored and added comments - 2

* Refactor album app bar

* Added modal for select user

* Implemented choose users

* Added additional share user button

* Added rule to show add users button
2022-07-22 09:44:22 -05:00
dependabot[bot]
02bde51caf Bump docker/build-push-action from 3.0.0 to 3.1.0 (#363)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: docker/build-push-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>
2022-07-20 20:49:11 -05:00
Matthias Rupp
bef1e2e3db Api logout route (#361)
* Add logout route that deletes http only cookies

* Rebuild API
2022-07-19 13:49:58 -05:00
Alex
be3e3e5d7e Added Cookie Authentication (#360)
* Added Cookie Authentication

* Fixed issue with bearer is in lower case

* Fixed bearer to Bearer to conform with standard
2022-07-18 14:14:25 -05:00
Alex
c028c7db4e dev/add detail viewer to album (#358)
* Rename asset viewer folder

* Refactor AssetViewer to be able to user with different component

* Refactor AssetViewer to be able to user with different component

* Added viewer for album and sharing
2022-07-18 00:22:39 -05:00
Alex Tran
c129023821 Remove console.log 2022-07-17 15:10:04 -05:00
Alex Tran
cbdb8fa51f Update get user info controller to avoid conflict with /count 2022-07-17 15:09:26 -05:00
Alex
c6ecfb679a Added sharing page to web (#355)
* Added shared album

* Added list tile

* Show info of shared album owner
2022-07-16 23:52:00 -05:00
Alex
5d03e9bda8 Fix test instance cannot clear database after each test" (#354)
* Update test

* Fixed test cannot initialize database

* Added a separate network to test containers group to run test while in development mode
2022-07-16 23:43:31 -05:00
Alex Tran
d8b26c6da8 Update bug report template 2022-07-16 10:54:00 -05:00
Alex
2e61cf3183 Update README.md
Fixed incorrect info about microservices container
2022-07-16 07:15:22 -05:00
Alex Tran
45e2335b86 Allow manually run test workflow 2022-07-16 00:48:35 -05:00
Alex Tran
2bbc44c5ab Fixed test 2022-07-16 00:45:58 -05:00
Alex Tran
012428416d Remove console.log 2022-07-15 23:27:23 -05:00
Alex
7134f93eb8 Add ablum feature to web (#352)
* Added album page

* Refactor sidebar

* Added album assets count info

* Added album viewer page

* Refactor album sorting

* Fixed incorrectly showing selected asset in album selection

* Improve fetching speed with prefetch

* Refactor to use ImmichThubmnail component for all

* Update to the latest version of Svelte

* Implement fixed app bar in album viewer

* Added shared user avatar

* Correctly get all owned albums, including shared
2022-07-15 23:18:17 -05:00
Jaime Baez
1887b5a860 Add email validation in the API when creating new users (#350)
* Refactor user.service - add user-repository

* Add email validation for creating users
2022-07-15 14:30:56 -05:00
Jaime Baez
ef17668871 Update Spanish translations (#348)
- add missing translations
- remove extra white spaces
- spelling and other corrections
2022-07-14 13:59:07 -05:00
Alex
e9909b179a Up version for release 2022-07-14 11:39:06 -05:00
Alex
09f8bdef6d Up version for release 2022-07-14 11:32:07 -05:00
Alex
2a9b09f359 Added DA,ES,FR,IT (#347)
* Added DA,ES,FR,IT

* Update French translation
2022-07-14 10:20:23 -05:00
Alex
1f6a3ccac7 Added local code in localizely config file 2022-07-14 08:39:43 -05:00
Eidenz
1f40fc1de9 Add missing translation tag for search_no_objects (#344)
* feat(mobile) added french translations

* fix(mobile) added missing translation tag for search_no_objects (EN,DE,FR)

* fix(mobile) renamed search_no_objects to search_page_no_objects
2022-07-13 20:46:28 -05:00
Eidenz
20b94ef0bb feat(mobile) added french translations (#343) 2022-07-13 11:35:02 -05:00
Alex Tran
72c334e5e0 Pump build number 2022-07-13 10:12:03 -05:00
Alex Tran
e7f35822af Pump version number 2022-07-13 10:04:25 -05:00
Alex Tran
bd2152d568 Pump build number 2022-07-13 09:56:34 -05:00
Alex
b1d7ef03e2 Pump version for release (#339)
* Remove unncessesary line

* Pump version for release
2022-07-13 09:51:41 -05:00
Alex Tran
aa74417d11 Fixed web production build 2022-07-13 08:35:52 -05:00
Alex Tran
229b009b7f Remove Axios import in web hook.ts 2022-07-13 08:25:43 -05:00
Fynn Petersen-Frey
bece6253d5 Improve Docker setup and reduce memory usage of production containers (#338) 2022-07-13 07:42:00 -05:00
Alex
ae7e582ec8 Refactor mobile to use OpenApi generated SDK (#336) 2022-07-13 07:23:48 -05:00
Fynn Petersen-Frey
d69470e207 Add extended redis & DB port configuration via environment variables (#330)
* Add database port configuration via env variable

Add redis connection configuration via env variables

* Add redis connection configuration via env variables
2022-07-12 22:21:11 -05:00
Alex
c60e852226 fix 331 (#335)
* fix #331 - Video with no date information in EXIF upload via web caused mobile client not able to render other assets
2022-07-12 16:34:43 -05:00
Zack Pollard
a205478a29 Remove advice regarding running immich-server with scaling (#334) 2022-07-11 11:40:25 -05:00
Alex
22d30522e1 [Localizely] Translations update (#324) 2022-07-10 22:31:29 -05:00
Matthias Rupp
19b1fad274 Add message to login screen (useful for demo instances) (#329)
* Add message for demo instances to login screen

* Rename env variable

* Added key into

* Add styling to conform with Immich color scheme

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-07-10 22:31:17 -05:00
Alex
9a6dfacf9b Refactor web to use OpenAPI SDK (#326)
* Refactor main index page

* Refactor admin page

* Refactor Auth endpoint

* Refactor directory to prep for monorepo

* Fixed refactoring path

* Resolved file path in vite

* Refactor photo index page

* Refactor thumbnail

* Fixed test

* Refactor Video Viewer component

* Refactor download file

* Refactor navigation bar

* Refactor upload file check

* Simplify Upload Asset signature

* PR feedback
2022-07-10 21:41:45 -05:00
Alex
7f236c5b18 Add OpenAPI Specs and Response DTOs (#320)
* Added swagger bearer auth method authentication accordingly

* Update Auth endpoint

* Added additional api information for authentication

* Added Swagger CLI pluggin

* Added DTO for /user endpoint

* Added /device-info reponse DTOs

* Implement server version

* Added DTOs for /server-info

* Added DTOs for /assets

* Added album to Swagger group

* Added generated specs file

* Add Client API generator for web

* Remove incorrectly placed node_modules

* Created class to handle access token

* Remove password and hash when getting all user

* PR feedback

* Fixed video from CLI doesn't get metadata extracted

* Fixed issue with TSConfig to work with generated openAPI

* PR feedback

* Remove console.log
2022-07-08 21:26:50 -05:00
Matthias Rupp
25985c732d Merge pull request #323 from alextran1502/fix/localizely_format
Change localizely format to json
2022-07-08 15:56:49 +02:00
Matthias
9ce50b7e3d Change localizely format to json 2022-07-08 15:53:20 +02:00
Matthias Rupp
f5e93a8179 Add translation keys for upload info section (#319) 2022-07-07 17:25:02 -05:00
Matthias Rupp
2b5cef156c Internationalization (German) of the mobile app. (#246)
* Add i18n framework to mobile app and write simple translation generator

* Replace all texts in login_form with i18n keys

* Localization of sharing section

* Localization of asset viewer section

* Use JSON as base translation format

* Add check for missing/unused translation keys

* Add localizely

* Remove i18n directory in favour of localizely

* Backup Translation

* More translations

* Translate home page

* Translation of search page

* Translate new server version announcement

* Reformat code

* Fix typo in german translation

* Update englisch translations

* Change translation keys to match dart filenames

* Add /api to translated endpoint_urls

* Update localizely.yml

* Add languages to ios plist

* Remove unused keys

* Added script to check outdated key in other translations

* Add download key to localizely.yml

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-07-07 13:40:54 -05:00
Alex
f3032f74a4 Added changelog for Fdroid 2022-07-06 22:35:07 -05:00
Alex
58ec7553ea Add information for uploading asset and error indication with error message for each failed upload. (#315)
* Added info box

* Fixed upload endpoint doesn't report error status code

* Added chip to show update error

* Added chip to show failed upload

* Add duplication check for upload

* Better duplication-checking placement

* Remove check for duplicated asset

* Added failed backup status route

* added page

* Display error card with thumbnail

* Improved styling

* Set thumbnail with better quality

* Remove force upload error
2022-07-06 16:12:55 -05:00
Alex
357f7d1c31 Added schedule job to perform reverse geocoding if key is added after backing up assets (#305) 2022-07-04 15:16:39 -05:00
Zack Pollard
e6d30d72fa Fix typeorm migrations (#297)
* fix: remove config parameter from typeorm cli and update config

the config parameter is no longer supported since version 0.3
the config now needs to export a DataSource object to work with the 0.3 cli

* fix: update all typeorm entities and migrations to be aligned with database structure

* Fixed test-util import databaseConfig

* Fixed column mismatch in raw query with new migration

* Remove dist build directory when starting dev server

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-07-04 14:20:43 -05:00
Jaime Baez
355038a91a Use npm ci for installing pacakages (#304) 2022-07-04 13:47:25 -05:00
Alex
97d9b80baa Added creation date for video from ffmpeg.prob (#303) 2022-07-04 13:44:43 -05:00
Alex Tran
b6814fad57 Up version for hotfix 2022-07-03 20:55:30 -05:00
Alex
7586c65103 Fix cannot query shared album on mobile (#298) 2022-07-03 20:52:03 -05:00
Alex
633170d743 Fixed inconnect image grouping with the same date but different year (#296) 2022-07-03 18:00:56 -05:00
Alex Tran
c5be7827c3 Remove 2284 to avoid confusion since 443 is not exposed from internal proxy 2022-07-03 11:37:26 -05:00
Alex Tran
e84c705e31 Added changelog to Fdroid 2022-07-03 10:49:37 -05:00
Alex Tran
36162509e0 Up version for release 2022-07-03 10:39:09 -05:00
Alex
76bf1c0379 Remove thumbnail generation on mobile app (#292)
* Remove thumbnail generation on mobile

* Remove tconditions for missing thumbnail on the backend

* Remove console.log

* Refactor queue systems

* Convert queue and processor name to constant

* Added corresponding interface to job queue
2022-07-02 21:06:36 -05:00
Alex
32b847c26e Fixed event propagation trigger navigating twice (#293) 2022-07-01 20:49:41 -05:00
Alex
a45d6fdf57 Fix server crash on bad file operation and other optimizations (#291)
* Fixed issue with generating thumbnail for video with 0 length cause undefined file and crash the server
* Added all file error handling operation
* Temporarily disabled WebSocket on the web because receiving a new upload event doesn't put the new file in the correct place. 
* Cosmetic fixed on the info panel
2022-07-01 12:00:12 -05:00
Zack Pollard
c071e64a7e infra: switch port to 3003 for machine learning container (#290)
* infra: switch port to 3003 for machine learning container

fixes #289

* Changed port of machine-learning-endpoint to match with new port

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-07-01 10:20:04 -05:00
Alex
663f12851e Fixed filename duplication when upload from web (#288)
* Fixed filename duplication when upload from web

* Fixed cosmetic of detail panel view
2022-06-30 20:43:33 -05:00
xpwmaosldk
c4ef523564 Optimize mobile - Avoid creating unnecessary widgets (#268)
* Avoid creating unnecessary widgets

* more flexible null handling and runtime errors prevention
2022-06-30 20:08:49 -05:00
Alex
992f792c0a Fixed admin is forced to change password on mobile app (#287)
* Fixed issues

* Upversion and add changed log
2022-06-30 13:59:02 -05:00
Alex Tran
97611fa057 Fixed issue with unexposed Nginx port on release image 2022-06-30 00:26:54 -05:00
584 changed files with 35022 additions and 8426 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,4 @@
# These are supported funding model platforms
github: alextran1502
custom: https://www.buymeacoffee.com/altran1502?new=1
custom: https://www.buymeacoffee.com/altran1502

View File

@@ -16,8 +16,11 @@ Note: Please search to see if an issue already exists for the bug you encountere
A clear and concise description of what the bug is.
**Task List**
*Please complete the task list below. We need this information to help us reproduce the bug or point out problems in your setup. You are not providing enough info may delay our effort to help you.*
- [ ] I have read thoroughly the README setup and installation instructions.
- [ ] If my setup is different, I have included my docker-compose file.
- [ ] I have included my `docker-compose` file.
- [ ] I have included my redacted `.env` file.
- [ ] I have included information on my machine, and environment.
@@ -34,13 +37,10 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Version [e.g. 22]
**System**
- Phone OS [iOS, Android]: `<version>`
- Server Version: `<version>`
- Mobile App Version: `<version>`
**Additional context**
Add any other context about the problem here.

View File

@@ -27,7 +27,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./server
file: ./server/Dockerfile
@@ -55,11 +55,11 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true
tags: |
altran1502/immich-machine-learning:latest
@@ -82,7 +82,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./web
file: ./web/Dockerfile
@@ -110,7 +110,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./nginx
file: ./nginx/Dockerfile

View File

@@ -24,17 +24,18 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- 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: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }}
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-server:staging
@@ -52,17 +53,18 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- 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: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name == 'pull_request' }}
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-machine-learning:staging
@@ -79,18 +81,19 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- 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: Build and Push Web
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: ${{ github.event_name == 'pull_request' }}
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-web:staging
@@ -107,16 +110,17 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- 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: Build and Push Proxy
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }}
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-proxy:staging
altran1502/immich-proxy:staging

View File

@@ -35,7 +35,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-server release
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./server
file: ./server/Dockerfile
@@ -68,11 +68,11 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: true
tags: |
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
@@ -107,7 +107,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-web release
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./web
file: ./web/Dockerfile
@@ -147,7 +147,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-proxy release
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
context: ./nginx
file: ./nginx/Dockerfile

19
.github/workflows/github-repo-stats.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: github-repo-stats
on:
schedule:
# Run this once per day, towards the end of the day for keeping the most
# recent data point most meaningful (hours are interpreted in UTC).
- cron: "0 23 * * *"
workflow_dispatch: # Allow for running this manually.
jobs:
j1:
name: github-repo-stats
runs-on: ubuntu-latest
steps:
- name: run-ghrs
# Use latest release.
uses: jgehrcke/github-repo-stats@RELEASE
with:
ghtoken: ${{ secrets.GHRS_GITHUB_API_TOKEN }}

View File

@@ -1,11 +1,13 @@
name: Test
on:
workflow_dispatch:
pull_request:
push: { branches: master }
push:
branches: [main]
jobs:
test-server-e2e:
name: Run test suite
e2e-tests:
name: Run end-to-end test suites
runs-on: ubuntu-latest
@@ -14,4 +16,15 @@ jobs:
uses: actions/checkout@v2
- name: Run Immich Server 2E2 Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
unit-tests:
name: Run unit test suites
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: cd server && npm install && npm run test

View File

@@ -1,20 +1,29 @@
dev:
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-new:
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-update:
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-scale:
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
stage:
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
pull-stage:
docker-compose -f ./docker/docker-compose.staging.yml pull
test-e2e:
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich_server_test --remove-orphans
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
prod:
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
prod-scale:
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=5 --scale immich-microservices=3 --remove-orphans
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
api:
cd ./server && npm run api:generate

290
README.md
View File

@@ -1,3 +1,9 @@
<h1 align="center"> Immich </h1>
<p align="center"> <b>High performance self-hosted photo and video backup solution.</b> </p>
<p align="center">
<img src="design/feature-panel.png" title="Immich Logo">
</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
<a href="https://github.com/alextran1502/immich"><img src="https://img.shields.io/github/stars/alextran1502/immich.svg?style=for-the-badge&logo=github&color=3F51B5&label=Stars&logoColor=000000&labelColor=ececec" alt="Star on Github"></a>
@@ -14,80 +20,68 @@
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Immich%20Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Immich Discord"/>
</a>
<br/>
<br/>
<br/>
<br/>
<p align="center">
<img src="design/feature-panel.png" title="Immich Logo">
</p>
<br/>
</p>
# Immich
**High performance self-hosted photo and video backup solution.**
![](https://media.giphy.com/media/y8ZeaAigGmNvlSoKhU/giphy.gif)
Loading ~4000 images/videos
## Screenshots
### Mobile
<p align="left">
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
<img src="design/selective-backup-screen.png" width="150" title="Backup Setting Info">
<img src="design/home-screen.jpeg" width="150" title="Home Screen">
<img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
<img src="design/shared-albums.png" width="150" title="Shared Albums">
<img src="design/nsc6.png" width="150" title="EXIF Info">
</p>
### Web
<p align="left">
<img src="design/web-home.jpeg" width="49%" title="Home Dashboard">
<img src="design/web-detail.jpeg" width="49%" title="Detail">
</p>
# Note
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
This project is under heavy development, there will be continuous functions, features and api changes.
## Content
- [Features](#features)
- [Screenshots](#screenshots)
- [Installation](#installation)
- [Mobile App](#-mobile-app)
- [Development](#development)
- [Support](#support)
- [Known Issues](#known-issues)
# Features
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes.
| | Mobile | Web |
| - | - | - |
| Upload and view videos and photos | Yes | Yes
| Auto backup when app is opened | Yes | N/A
| Selective album(s) for backup | Yes | N/A
| Download photos and videos to local device | Yes | Yes
| Multi-user support | Yes | Yes
| Shared Albums | Yes | No
| Quick navigation with draggable scrollbar | Yes | Yes
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
| Metadata view (EXIF, map) | Yes | Yes
| Search by metadata, objects and image tags | Yes | No
| Administrative functions (user management) | No | Yes
| - | - | - |
| ☁️ Upload and view videos and photos | Yes | Yes
| 🔄 Auto backup when the app is opened | Yes | N/A
| ☑️ Selective album(s) for backup | Yes | N/A
| ⬇️ Download photos and videos to local device | Yes | Yes
| 👪 Multi-user support | Yes | Yes
| 🖼️ Album | Yes | Yes
| 🤝 Shared Albums | Yes | Yes
| 🚀 Quick navigation with draggable scrollbar | Yes | Yes
| 🗃️ Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
| 🧭 Metadata view (EXIF, map) | Yes | Yes
| 🔎 Search by metadata, objects and image tags | Yes | No
| ⚙️ Administrative functions (user management) | N/A | Yes
# System Requirement
<br/>
**OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
# Screenshots
I haven't tested with `Docker for Windows` as well as `WSL` on Windows
### Mobile
| | | | | |
| - | - | - | - | - |
| <img src="design/login-screen.png" width="150" title="Login With Custom URL"> <p align="center"> Login with custom URL </p> | <img src="design/backup-screen.png" width="150" title="Backup Setting Info"> <p align="center"> Backup Settings </p> | <img src="design/selective-backup-screen.png" width="150" title="Backup Setting Info"> <p align="center"> Backup selection </p> | <img src="design/home-screen.jpeg" width="150" title="Home Screen"> <p align="center"> Home Screen </p> | <img src="design/search-screen.jpeg" width="150" title="Curated Search Info"> <p align="center"> Curated search </p> |
| <img src="design/shared-albums.png" width="150" title="Shared Albums"> <p align="center"> Shared albums </p> | <img src="design/nsc6.png" width="150" title="EXIF Info"> <p align="center"> EXIF info </p> | <img src="https://media.giphy.com/media/y8ZeaAigGmNvlSoKhU/giphy.gif" width="150" title="Loading ~4000 images/videos"> <p align="center"> Loading ~4000 images/videos </p> |
*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Docker image on arm64v7 yet.*
### Web
| Home Dashboard | Image view |
| - | - |
|<img src="design/web-home.jpeg" width="100%" title="Home Dashboard"> | <img src="design/web-detail.jpeg" width="100%" title="Detail">|
**RAM**: At least 2GB, preffered 4GB.
**Core**: At least 2 cores, preffered 4 cores.
<br/>
# Getting Started
# Project Details
## 💾 System Requirements
You can use docker compose for development and testing out the application, there are several services that compose Immich:
- **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
- **RAM**: At least 2GB, preferred 4GB.
- **Core**: At least 2 cores, preferred 4 cores.
## 🔩 Technology Stack
There are several services that compose Immich:
1. **NestJs** - Backend of the application
2. **SvelteKit** - Web frontend of the application
@@ -96,130 +90,94 @@ You can use docker compose for development and testing out the application, ther
5. **Nginx** - Load balancing and optimized file uploading.
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
## Step 1: Populate .env file
Navigate to `docker` directory and run
<br/>
```
cp .env.example .env
```
# Installation
Then populate the value in there.
## Testing One-step installation (not recommended for production)
Notice that if set `ENABLE_MAPBOX` to `true`, you will have to provide `MAPBOX_KEY` for the server to run.
> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
*Applicable system: Ubuntu, Debian, MacOS*
**Example**
- In the shell, from the directory of your choice, run the following command:
```bash
###################################################################################
# Database
###################################################################################
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=immich
###################################################################################
# Upload File Config
###################################################################################
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
###################################################################################
# JWT SECRET
###################################################################################
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
###################################################################################
# MAPBOX
####################################################################################
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false
MAPBOX_KEY=
###################################################################################
# WEB
###################################################################################
# This is the URL of your vm/server where you host Immich, so that the web frontend
# know where can it make the request to.
# For example: If your server IP address is 10.1.11.50, the environment variable will
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283/api
curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | bash
```
## Step 2: Start the server
This script will download the `docker-compose.yml` file and the `.env` file, then populate the necessary information, and finally run the `docker-compose up` or `docker compose up` (based on your docker's version) command.
To **start**, run
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`.
The directory which is used to store the backup file is `./immich-app/immich-data`.
<br/>
## Custom installation (Recommended)
### Step 1 - Download necessary files
- Create a directory called `immich-app` and cd into it.
- Get `docker-compose.yml`
```bash
docker-compose -f ./docker/docker-compose.yml up
wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml
```
If you have a few thousand photos/videos, I suggest running docker-compose with *scaling* option for the `immich_server` container to handle high I/O load when using fast scrolling.
- Get `.env`
```bash
docker-compose -f ./docker/docker-compose.yml up --scale immich-server=5
wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
```
To *update* docker-compose with newest image (if you have started the docker-compose previously)
### Step 2 - Populate .env file with custom information
```bash
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
```
<a href="https://github.com/immich-app/immich/blob/main/docker/.env.example" target="_blank"><b>See the example <code>.env</code> file</b></a>
The server will be running at `http://your-ip:2283/api` through `Nginx`
* Populate custom database information if necessary.
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
* [Optional] Populate Mapbox value to use reverse geocoding.
## Step 3: Register User
### Step 3 - Start the containers
Access the web interface at `http://your-ip:2283` to register an admin account.
- Run `docker-compose up` or `docker compose up` (based on your docker's version)
<p align="left">
### Step 4 - Register admin user
- Navigate to the web at `http://<machine-ip-address>:2283` and follow the prompts to register admin user.
<p align="center">
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
<p/>
</p>
Additional accounts on the server can be created by the admin account.
- You can add and manage users from the administration page.
<p align="center">
<img src="design/admin-interface.png" width="500" title="Admin User Management">
</p>
<p align="left">
<img src="design/admin-interface.png" width="500" title="Admin User Management">
<p/>
### Step 5 - Access the mobile app
## Step 4: Run mobile app
Login the mobile app with your server address
<p align="left">
- Login the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
<p align="center">
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
<p/>
</p>
## F-Droid
You can get the app on F-droid by clicking the image below.
<br/>
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/app.alextran.immich)
# Mobile app
| F-Droid | Google Play | iOS |
| - | - | - |
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <img src="design/google-play-qr-code.png" width="200" title="Google Play Store"> <p/> | <p align="left"> <img src="design/ios-qr-code.png" width="200" title="Apple App Store"> <p/> |
> *The App version might be lagging behind the latest release due to the review process.*
## Android
#### Get the app on Google Play Store [here](https://play.google.com/store/apps/details?id=app.alextran.immich)
*The App version might be lagging behind the latest release due to the review process.*
<p align="left">
<img src="design/google-play-qr-code.png" width="200" title="Google Play Store">
<p/>
## iOS
#### Get the app on Apple AppStore [here](https://apps.apple.com/us/app/immich/id1613945652):
*The App version might be lagging behind the latest release due to the review process.*
<p align="left">
<img src="design/ios-qr-code.png" width="200" title="Apple App Store">
<p/>
<br/>
# Development
@@ -231,21 +189,41 @@ make dev # required Makefile installed on the system.
All servers and web container are hot reload for quick feedback loop.
## Note for developers
### 1 - OpenAPI
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). When you add a new or modify an existing endpoint, you must run the generate command below to update the client SDK.
```bash
npm run api:generate # Run from server directory
```
You can find the generated client SDK in the [`web/src/api`](web/src/api) for Typescript SDK and [`mobile/openapi`](mobile/openapi) for Dart SDK.
<br/>
# Support
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**one time**](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or monthly donation from [**Github Sponsor**](https://github.com/sponsors/alextran1502).
You can also donate using crypto currency with the following addresses:
<p align="" style="display: flex; place-items: center; gap: 15px" title="Bitcoin(BTC)"><img src="design/bitcoin.png" width="25" title="Bitcoin"> <b>Bitcoin</b>: <code>1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX</code></p>
<p align="" style="display: flex; place-items: center; gap: 15px" title="Cardano(ADA)"> <img src="design/cardano.png" width="30" title="Cardano"> <b>Cardano</b>: <code>addr1qyy567vqhqrr3p7vpszr5p264gw89sqcwts2z8wqy4yek87cdmy79zazyjp7tmwhkluhk3krvslkzfvg0h43tytp3f5q49nycc</code> </p>
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/altran1502)
This is also a meaningful way to give me motivation and encouragement to continue working on the app.
Cheers! 🎉
# Known Issue
<br/>
# Known Issues
## TensorFlow Build Issue
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup*
*This is a known issue for incorrect Promox setup*
TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
@@ -258,7 +236,3 @@ If you are running virtualization in Promox, the VM doesn't have the flag enable
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
Otherwise you can:
- edit `docker-compose.yml` file and comment the whole `immich-machine-learning` service **which will disable machine learning features like object detection and image classification**
- switch to a different VM/desktop with different architecture.

BIN
design/bitcoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
design/cardano.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -7,6 +7,8 @@ DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=immich
# Optional Database settings:
# DB_PORT=5432
@@ -17,6 +19,12 @@ DB_DATABASE_NAME=immich
REDIS_HOSTNAME=immich_redis
# Optional Redis settings:
# REDIS_PORT=6379
# REDIS_DBINDEX=0
# REDIS_PASSWORD=
# REDIS_SOCKET=
@@ -48,16 +56,11 @@ ENABLE_MAPBOX=false
MAPBOX_KEY=
####################################################################################
# WEB - Optional
####################################################################################
# Custom message on the login page, should be written in HTML form.
# For example VITE_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
###################################################################################
# WEB
###################################################################################
# This is the URL of your vm/server where you host Immich, so that the web frontend
# know where can it make the request to.
# For example: If your server IP address is 10.1.11.50, the environment variable will
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
VITE_SERVER_ENDPOINT=
VITE_LOGIN_PAGE_MESSAGE=

View File

@@ -1,5 +1,5 @@
# Database
DB_HOSTNAME=immich_postgres_test
DB_HOSTNAME=immich-database-test
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=e2e_test

View File

@@ -70,6 +70,8 @@ services:
- ../web:/usr/src/app
- /usr/src/app/node_modules
restart: always
depends_on:
- immich-server
redis:
container_name: immich_redis

View File

@@ -1,11 +1,12 @@
version: "3.8"
services:
immich_server_test:
image: immich-server-dev:latest
immich-server-test:
image: immich-server-test
build:
context: ../server
dockerfile: Dockerfile
target: builder
command: npm run test:e2e
expose:
- "3000"
@@ -17,15 +18,17 @@ services:
environment:
- NODE_ENV=development
depends_on:
- redis
- database
redis:
container_name: immich_redis_test
- immich-redis-test
- immich-database-test
networks:
- immich-test-network
immich-redis-test:
container_name: immich-redis-test
image: redis:6.2
database:
container_name: immich_postgres_test
networks:
- immich-test-network
immich-database-test:
container_name: immich-database-test
image: postgres:14
env_file:
- .env.test
@@ -36,5 +39,8 @@ services:
PG_DATA: /var/lib/postgresql/data
volumes:
- /var/lib/postgresql/data
ports:
- 5432:5432
networks:
- immich-test-network
networks:
immich-test-network:

View File

@@ -73,7 +73,6 @@ services:
image: altran1502/immich-proxy:release
ports:
- 2283:80
- 2284:443
logging:
driver: none
depends_on:

83
install.sh Executable file
View File

@@ -0,0 +1,83 @@
echo "Starting Immich installation..."
ip_address=$(hostname -I | awk '{print $1}')
RED='\033[0;31m'
GREEN='\032[0;31m'
NC='\033[0m' # No Color
machine_has() {
type "$1" >/dev/null 2>&1
}
create_immich_directory() {
echo "Creating Immich directory..."
mkdir -p ./immich-app/immich-data
}
download_docker_compose_file() {
echo "Downloading docker-compose.yml..."
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
}
download_dot_env_file() {
echo "Downloading .env file..."
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
}
populate_upload_location() {
echo "Populating default UPLOAD_LOCATION value..."
cd ./immich-app/immich-data
upload_location=$(pwd)
# Replace value of UPLOAD_LOCATION in .env with upload_location path
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
else
sed -i "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
fi
cd ..
}
start_docker_compose() {
echo "Starting Immich's docker containers"
if machine_has "docker compose"; then {
docker compose up --remove-orphans -d
show_friendly_message
exit 0
}; fi
if machine_has "docker-compose"; then
docker-compose up --remove-orphans -d
show_friendly_message
exit 0
fi
}
show_friendly_message() {
echo "Succesfully deployed Immich!"
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
echo "The backup (or upload) location is $upload_location"
echo "---------------------------------------------------"
echo "If you want to confgure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
1. First bring down the containers with the command 'docker-compose down' in the immich-app directory,
2. Then change the information that fits your needs in the '.env' file,
3. Finally, bring the containers back up with the command 'docker-compose up --remove-orphans -d' in the immich-app directory"
}
# MAIN
create_immich_directory
download_docker_compose_file
download_dot_env_file
populate_upload_location
start_docker_compose

19
localizely.yml Normal file
View File

@@ -0,0 +1,19 @@
config_version: 1.0
project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7
file_type: json
upload:
files:
- file: mobile/assets/i18n/en-US.json
locale_code: en-US
- file: mobile/assets/i18n/de-DE.json
locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR
download:
files:
- file: mobile/assets/i18n/en-US.json
locale_code: en-US
- file: mobile/assets/i18n/de-DE.json
locale_code: de-DE
- file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR

View File

@@ -1,4 +1,5 @@
FROM node:16-bullseye-slim
# Build stage
FROM node:16-bullseye-slim as builder
ARG DEBIAN_FRONTEND=noninteractive
@@ -9,8 +10,33 @@ COPY package.json package-lock.json ./
RUN apt-get update
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
RUN npm install
RUN npm ci
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
COPY . .
RUN npm run build
# Prod stage
FROM node:16-bullseye-slim
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
COPY entrypoint.sh ./
RUN mkdir -p /usr/src/app/dist \
&& mkdir -p /usr/src/app/node_modules \
&& apt-get update \
&& apt-get install -y ffmpeg \
&& rm -rf /var/cache/apt/lists
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/dist ./dist
RUN npm prune --production
# CMD [ "node", "dist/main" ]

View File

@@ -1,2 +1,3 @@
# npm run typeorm migration:run
npm run build && npm run start:prod
# npm run start:prod
node dist/main.js

File diff suppressed because it is too large Load Diff

View File

@@ -28,11 +28,11 @@
"@nestjs/typeorm": "^8.0.3",
"@tensorflow-models/coco-ssd": "^2.2.2",
"@tensorflow-models/mobilenet": "^2.1.0",
"@tensorflow/tfjs": "^3.15.0",
"@tensorflow/tfjs-converter": "^3.15.0",
"@tensorflow/tfjs-core": "^3.15.0",
"@tensorflow/tfjs-node": "^3.15.0",
"@tensorflow/tfjs-node-gpu": "^3.15.0",
"@tensorflow/tfjs": "^3.19.0",
"@tensorflow/tfjs-converter": "^3.19.0",
"@tensorflow/tfjs-core": "^3.19.0",
"@tensorflow/tfjs-node": "^3.19.0",
"@tensorflow/tfjs-node-gpu": "^3.19.0",
"@trpc/server": "^9.20.3",
"pg": "^8.7.3",
"reflect-metadata": "^0.1.13",

View File

@@ -3,7 +3,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const databaseConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: process.env.DB_HOSTNAME || 'immich_postgres',
port: 5432,
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE_NAME,

View File

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

2
mobile/.gitignore vendored
View File

@@ -24,7 +24,7 @@
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
**/ios/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies

View File

@@ -21,10 +21,18 @@ linter:
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
use_build_context_synchronously: false
require_trailing_commas: true
unrelated_type_equality_checks: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
- openapi/
- openapi/test/
- lib/generated_plugin_registrant.dart

View File

@@ -80,5 +80,8 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
implementation "com.google.guava:guava:$guava_version"
}

View File

@@ -1,6 +0,0 @@
package com.example.immich_mobile
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,98 @@
package app.alextran.immich
import android.content.Context
import android.net.Uri
import android.content.Intent
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
/**
* Android plugin for Dart `BackgroundService`
*
* Receives messages/method calls from the foreground Dart side to manage
* the background service, e.g. start (enqueue), stop (cancel)
*/
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private var methodChannel: MethodChannel? = null
private var context: Context? = null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
}
private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) {
context = ctx
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
methodChannel?.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
onDetachedFromEngine()
}
private fun onDetachedFromEngine() {
methodChannel?.setMethodCallHandler(null)
methodChannel = null
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
val ctx = context!!
when(call.method) {
"initialize" -> { // needs to be called prior to any other method
val args = call.arguments<ArrayList<*>>()!!
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit().putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long).apply()
result.success(true)
}
"start" -> {
val args = call.arguments<ArrayList<*>>()!!
val immediate = args.get(0) as Boolean
val keepExisting = args.get(1) as Boolean
val requireUnmeteredNetwork = args.get(2) as Boolean
val requireCharging = args.get(3) as Boolean
val notificationTitle = args.get(4) as String
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit().putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, notificationTitle).apply()
BackupWorker.startWork(ctx, immediate, keepExisting, requireUnmeteredNetwork, requireCharging)
result.success(true)
}
"stop" -> {
BackupWorker.stopWork(ctx)
result.success(true)
}
"isEnabled" -> {
result.success(BackupWorker.isEnabled(ctx))
}
"disableBatteryOptimizations" -> {
if(!BackupWorker.isIgnoringBatteryOptimizations(ctx)) {
val args = call.arguments<ArrayList<*>>()!!
val text = args.get(0) as String
Toast.makeText(ctx, text, Toast.LENGTH_LONG).show()
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.setData(Uri.parse("package:" + ctx.getPackageName()))
try {
ctx.startActivity(intent)
} catch(e: Exception) {
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
try {
ctx.startActivity(intent)
} catch (e2: Exception) {
return result.success(false)
}
}
}
result.success(true)
}
else -> result.notImplemented()
}
}
}
private const val TAG = "BackgroundServicePlugin"

View File

@@ -0,0 +1,345 @@
package app.alextran.immich
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.provider.MediaStore
import android.provider.BaseColumns
import android.provider.MediaStore.MediaColumns
import android.provider.MediaStore.Images.Media
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.concurrent.futures.ResolvableFuture
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ForegroundInfo
import androidx.work.ListenableWorker
import androidx.work.NetworkType
import androidx.work.WorkerParameters
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.google.common.util.concurrent.ListenableFuture
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.view.FlutterCallbackInformation
import java.util.concurrent.TimeUnit
/**
* Worker executed by Android WorkManager to perform backup in background
*
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
* `background.service.dart` to run the actual backup logic.
* Called by Android WorkManager when all constraints for the work are met,
* i.e. a new photo/video is created on the device AND battery is not low.
* Optionally, unmetered network (wifi) and charging can be required.
* As this work is not triggered periodically, but on content change, the
* worker enqueues itself again with the same settings.
* In case the worker is stopped by the system (e.g. constraints like wifi
* are no longer met, or the system needs memory resources for more other
* more important work), the worker is replaced without the constraint on
* changed contents to run again as soon as deemed possible by the system.
*/
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
private val resolvableFuture = ResolvableFuture.create<Result>()
private var engine: FlutterEngine? = null
private lateinit var backgroundChannel: MethodChannel
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
val ctx = applicationContext
// enqueue itself once again to continue to listen on added photos/videos
enqueueMoreWork(ctx,
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false))
if (!flutterLoader.initialized()) {
flutterLoader.startInitialization(ctx)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create a Notification channel if necessary
createChannel()
}
if (isIgnoringBatteryOptimizations) {
// normal background services can only up to 10 minutes
// foreground services are allowed to run indefinitely
// requires battery optimizations to be disabled (either manually by the user
// or by the system learning that immich is important to the user)
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
setForegroundAsync(createForegroundInfo(title))
}
engine = FlutterEngine(ctx)
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
runDart()
}
return resolvableFuture
}
/**
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
* `background.service.dart` to run the actual backup logic.
*/
private fun runDart() {
val callbackDispatcherHandle = applicationContext.getSharedPreferences(
SHARED_PREF_NAME, Context.MODE_PRIVATE).getLong(SHARED_PREF_CALLBACK_KEY, 0L)
val callbackInformation = FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle)
val appBundlePath = flutterLoader.findAppBundlePath()
engine?.let { engine ->
backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel")
backgroundChannel.setMethodCallHandler(this@BackupWorker)
engine.dartExecutor.executeDartCallback(
DartExecutor.DartCallback(
applicationContext.assets,
appBundlePath,
callbackInformation
)
)
}
}
override fun onStopped() {
// called when the system has to stop this worker because constraints are
// no longer met or the system needs resources for more important tasks
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
backgroundChannel.invokeMethod("systemStop", null)
}
// cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException)
// instead, wait for 5 seconds until forcefully stopping backup work
Handler(Looper.getMainLooper()).postDelayed({
stopEngine(null)
}, 5000)
}
private fun stopEngine(result: Result?) {
if (result != null) {
resolvableFuture.set(result)
} else if (engine != null && inputData.getInt(DATA_KEY_RETRIES, 0) == 0) {
// stopped by system and this is the first time (content change constraints active)
// replace the task without the content constraints to finish the backup as soon as possible
enqueueMoreWork(applicationContext,
immediate = true,
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
initialDelayInMs = ONE_MINUTE,
retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
}
engine?.destroy()
engine = null
}
override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
when (call.method) {
"initialized" ->
backgroundChannel.invokeMethod(
"onAssetsChanged",
null,
object : MethodChannel.Result {
override fun notImplemented() {
stopEngine(Result.failure())
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
stopEngine(Result.failure())
}
override fun success(receivedResult: Any?) {
val success = receivedResult as Boolean
stopEngine(if(success) Result.success() else Result.retry())
if (!success && inputData.getInt(DATA_KEY_RETRIES, 0) == 0) {
// there was an error (e.g. server not available)
// replace the task without the content constraints to finish the backup as soon as possible
enqueueMoreWork(applicationContext,
immediate = true,
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
initialDelayInMs = ONE_MINUTE,
retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
}
}
}
)
"updateNotification" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args.get(0) as String
val content = args.get(1) as String
if (isIgnoringBatteryOptimizations) {
setForegroundAsync(createForegroundInfo(title, content))
}
}
"showError" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args.get(0) as String
val content = args.get(1) as String
val individualTag = args.get(2) as String?
showError(title, content, individualTag)
}
"clearErrorNotifications" -> clearErrorNotifications()
else -> r.notImplemented()
}
}
private fun showError(title: String, content: String, individualTag: String?) {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setOnlyAlertOnce(true)
.build()
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
}
private fun clearErrorNotifications() {
notificationManager.cancel(NOTIFICATION_ERROR_ID)
}
private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
return ForegroundInfo(NOTIFICATION_ID, notification)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(foreground)
val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(error)
}
companion object {
const val SHARED_PREF_NAME = "immichBackgroundService"
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
private const val TASK_NAME = "immich/photoListener"
private const val DATA_KEY_UNMETERED = "unmetered"
private const val DATA_KEY_CHARGING = "charging"
private const val DATA_KEY_RETRIES = "retries"
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2
private const val ONE_MINUTE: Long = 60000
/**
* Enqueues the `BackupWorker` to run when all constraints are met.
*
* @param context Android Context
* @param immediate whether to enqueue(replace) the worker without the content change constraint
* @param keepExisting if true, use `ExistingWorkPolicy.KEEP`, else `ExistingWorkPolicy.APPEND_OR_REPLACE`
* @param requireUnmeteredNetwork if true, task only runs if connected to wifi
* @param requireCharging if true, task only runs if device is charging
* @param retries retry count (should be 0 unless an error occured and this is a retry)
*/
fun startWork(context: Context,
immediate: Boolean = false,
keepExisting: Boolean = false,
requireUnmeteredNetwork: Boolean = false,
requireCharging: Boolean = false) {
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, true).apply()
enqueueMoreWork(context, immediate, keepExisting, requireUnmeteredNetwork, requireCharging)
}
private fun enqueueMoreWork(context: Context,
immediate: Boolean = false,
keepExisting: Boolean = false,
requireUnmeteredNetwork: Boolean = false,
requireCharging: Boolean = false,
initialDelayInMs: Long = 0,
retries: Int = 0) {
if (!isEnabled(context)) {
return
}
val constraints = Constraints.Builder()
.setRequiredNetworkType(if (requireUnmeteredNetwork) NetworkType.UNMETERED else NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(requireCharging);
if (!immediate) {
constraints
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
}
val inputData = Data.Builder()
.putBoolean(DATA_KEY_CHARGING, requireCharging)
.putBoolean(DATA_KEY_UNMETERED, requireUnmeteredNetwork)
.putInt(DATA_KEY_RETRIES, retries)
.build()
val photoCheck = OneTimeWorkRequest.Builder(BackupWorker::class.java)
.setConstraints(constraints.build())
.setInputData(inputData)
.setInitialDelay(initialDelayInMs, TimeUnit.MILLISECONDS)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
ONE_MINUTE,
TimeUnit.MILLISECONDS)
.build()
val policy = if (immediate) ExistingWorkPolicy.REPLACE else (if (keepExisting) ExistingWorkPolicy.KEEP else ExistingWorkPolicy.APPEND_OR_REPLACE)
val op = WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME, policy, photoCheck)
val result = op.getResult().get()
}
/**
* Stops the currently running worker (if any) and removes it from the work queue
*/
fun stopWork(context: Context) {
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply()
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME)
}
/**
* Returns `true` if the app is ignoring battery optimizations
*/
fun isIgnoringBatteryOptimizations(ctx: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pwrm = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
val name = ctx.packageName
return pwrm.isIgnoringBatteryOptimizations(name)
}
return true
}
/**
* Return true if the user has enabled the background backup service
*/
fun isEnabled(ctx: Context): Boolean {
return ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)
}
private val flutterLoader = FlutterLoader()
}
}
private const val TAG = "BackupWorker"

View File

@@ -1,6 +1,13 @@
package app.alextran.immich
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.getPlugins().add(BackgroundServicePlugin())
}
}

View File

@@ -1,5 +1,8 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.work_version = '2.7.1'
ext.concurrent_version = '1.1.0'
ext.guava_version = '31.0.1-android'
repositories {
google()
mavenCentral()

View File

@@ -1,2 +1,2 @@
json_key_file("/Users/alex/Documents/immich-fastlane-googleplaystore-key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("app.alextran.immich") # e.g. com.krausefx.app
json_key_file("/Users/alex/Documents/immich-play-store-key.json")
package_name("app.alextran.immich")

View File

@@ -16,10 +16,25 @@
default_platform(:android)
platform :android do
desc "Build Android"
lane :build do
gradle(
task: 'bundle',
build_type: 'Release',
)
end
desc "Update AAB to PlayStore"
lane :beta do
upload_to_play_store(track: 'beta', aab: '../build/app/outputs/bundle/release/app-release.aab')
desc "Build and Release Android"
lane :release do
gradle(
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 35,
"android.injected.version.name" => "1.25.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')
end
end

View File

@@ -15,13 +15,21 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
## Android
### android beta
### android build
```sh
[bundle exec] fastlane android beta
[bundle exec] fastlane android build
```
Update AAB to PlayStore
Build Android
### android release
```sh
[bundle exec] fastlane android release
```
Build and Release Android
----

View File

@@ -0,0 +1,2 @@
* Fixed admin is forced to change password upon logging in on mobile app
* Fixed change password form validation

View File

@@ -0,0 +1 @@
* Removed thumbnail generation on mobile - the operation now will be on the server to reduce CPU load and battery usage.

View File

@@ -0,0 +1 @@
* Hot fix: Restore shared album functionality

View File

@@ -0,0 +1 @@
* Add information for uploading asset and error indication with error message for each failed upload.

View File

@@ -0,0 +1 @@
* Refactored app to use OpenAPI SDK to improve performance and project structure.

View File

@@ -0,0 +1 @@
* Refactored app to use OpenAPI SDK to improve performance and project structure.

View File

@@ -0,0 +1 @@
* Added other languages to app

View File

@@ -0,0 +1 @@
* Added French, Danish, Spanish, French, Japanese, Polish, and Finish translation to the app

View File

@@ -0,0 +1,2 @@
* New feature - Gallery view now enable with swipping action
* New feature - Add album feature

View File

@@ -0,0 +1,3 @@
* Improve performance
* Fix album title overflow
* New feature - Share asset from mobile to other apps

View File

@@ -0,0 +1 @@
* Modify Album API endpoint to return count attribute instead of all assets to reduce network consumption and CPU processing.

View File

@@ -0,0 +1,2 @@
* Added setting screen
* Implemented dark mode

View File

@@ -0,0 +1,3 @@
* Feature - [Android] Background backup.
* Fixed - [iOS] Dark mode not auto switch.
* Fixed - WebSocket not getting correct data on mobile.

View File

@@ -0,0 +1,4 @@
* Feature - Customization options for asset grid
* Added pt-BR Translation: Translation into Portuguese Brazil
* Feature - Show notifications on background backup errors
* Optimization - Use CachedNetworkImage and separate cache for thumbnails on library page

View File

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

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "EKSKLUDERET",
"album_info_card_backup_album_included": "INKLUDERET",
"album_viewer_appbar_share_delete": "Slet album",
"album_viewer_appbar_share_err_delete": "Fejlede sletning af album",
"album_viewer_appbar_share_err_leave": "Fejlede i at forlade album",
"album_viewer_appbar_share_err_remove": "Der er problemer med at fjerne elementer fra album",
"album_viewer_appbar_share_err_title": "Fejlede i at ændre albumtitel",
"album_viewer_appbar_share_leave": "Forlad album",
"album_viewer_appbar_share_remove": "Fjern fra album",
"album_viewer_page_share_add_users": "Tilføj brugere",
"backup_album_selection_page_albums_device": "Albummer på enhed ({})",
"backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere",
"backup_album_selection_page_assets_scatter": "Elementer kan være spredt på tværs af flere albummer. Albummer kan således inkluderes eller udelukkes under sikkerhedskopieringsprocessen.",
"backup_album_selection_page_select_albums": "Vælg albummer",
"backup_album_selection_page_selection_info": "Oplysninger om valgte",
"backup_album_selection_page_total_assets": "Samlede unikke elementer",
"backup_all": "Alt",
"backup_controller_page_albums": "Sikkerhedskopier albummer",
"backup_controller_page_backup": "Sikkerhedskopier",
"backup_controller_page_backup_selected": "Valgte: ",
"backup_controller_page_backup_sub": "Sikkerhedskopierede billeder og videoer",
"backup_controller_page_cancel": "Annuller",
"backup_controller_page_created": "Oprettet den: {}",
"backup_controller_page_desc_backup": "Slå sikkerhedskopiering til automatisk at uploade nye elementer til serveren.",
"backup_controller_page_excluded": "Ekskluderet: ",
"backup_controller_page_failed": "Felet ({})",
"backup_controller_page_filename": "Filnavn: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Sikkerhedskopieringsinformation",
"backup_controller_page_none_selected": "Ingen valgte",
"backup_controller_page_remainder": "Tilbageværende",
"backup_controller_page_remainder_sub": "Tilbageværende billeder og albummer, at sikkerhedskopiere, fra valgte",
"backup_controller_page_select": "Vælg",
"backup_controller_page_server_storage": "Serverlager",
"backup_controller_page_start_backup": "Start sikkerhedskopiering",
"backup_controller_page_status_off": "Sikkerhedskopiering er slået fra",
"backup_controller_page_status_on": "Sikkerhedskopiering er slået til",
"backup_controller_page_storage_format": "{} af {} brugt",
"backup_controller_page_to_backup": "Albummer at sikkerhedskopiere",
"backup_controller_page_total": "I alt",
"backup_controller_page_total_sub": "Alle unikke billeder og videoer fra valgte albummer",
"backup_controller_page_turn_off": "Slå sikkerhedskopiering fra",
"backup_controller_page_turn_on": "Slå sikkerhedskopiering til",
"backup_controller_page_uploading_file_info": "Uploader filinformation",
"backup_err_only_album": "Kan ikke slette det eneste album",
"backup_info_card_assets": "elementer",
"control_bottom_app_bar_delete": "Slet",
"create_shared_album_page_share": "Del",
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
"create_shared_album_page_share_select_photos": "Vælg billeder",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E d. LLL y • hh:mm",
"delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed",
"delete_dialog_cancel": "Annuller",
"delete_dialog_ok": "Slet",
"delete_dialog_title": "Slet permanent",
"exif_bottom_sheet_description": "Tilføj beskrivelse...",
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "LOKATION",
"login_form_button_text": "Log ind",
"login_form_email_hint": "din-email@email.com",
"login_form_endpoint_hint": "http://din-server-ip:port/api",
"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_leading_whitespace": "Mellemrum før",
"login_form_err_trailing_whitespace": "Mellemrum efter",
"login_form_failed_login": "Der opstod en vejl ved at logge ind. Tjek server URL, email og kodeordet",
"login_form_label_email": "Email",
"login_form_label_password": "Kodeord",
"login_form_password_hint": "kodeord",
"login_form_save_login": "Forbliv logget ind",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Klient og server er ajour",
"profile_drawer_sign_out": "Log ud",
"search_bar_hint": "Søg i dine billeder",
"search_page_no_objects": "Ingen elementer er tilgængelige",
"search_page_no_places": "Ingen placeringsinformation er tilgængelig",
"search_page_places": "Steder",
"search_page_things": "Ting",
"search_result_page_new_search_hint": "Ny søgning",
"select_additional_user_for_sharing_page_suggestions": "Anbefalinger",
"select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album",
"select_user_for_sharing_page_share_suggestions": "Anbefalinger",
"share_add": "Tilføj",
"share_add_photos": "Tilføj billeder",
"share_add_title": "Tilføj en titel",
"share_create_album": "Opret album",
"share_invite": "Inviter til album",
"sharing_page_album": "Delt albums",
"sharing_page_description": "Opret delte albummer for at dele billeder og video med personer på dit netværk.",
"sharing_page_empty_list": "TOM LISTE",
"sharing_silver_appbar_create_shared_album": "Opret delt album",
"sharing_silver_appbar_share_partner": "Del med partner",
"tab_controller_nav_photos": "Billeder",
"tab_controller_nav_search": "Søg",
"tab_controller_nav_sharing": "Deling",
"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_title": "Ny serverversion er tilgængelig \uD83C\uDF89"
}

View File

@@ -0,0 +1,126 @@
{
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
"album_viewer_appbar_share_delete": "Album löschen",
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
"album_viewer_appbar_share_err_remove": "Beim Löschen von Elementen aus dem Album ist ein Problem aufgetreten",
"album_viewer_appbar_share_err_title": "Der Titel konnte nicht geändert werden",
"album_viewer_appbar_share_leave": "Album verlassen",
"album_viewer_appbar_share_remove": "Entferne vom Album",
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
"backup_album_selection_page_select_albums": "Alben auswählen",
"backup_album_selection_page_selection_info": "Auswahl",
"backup_album_selection_page_total_assets": "Elemente",
"backup_all": "Alle",
"backup_controller_page_albums": "Gesicherte Alben",
"backup_controller_page_backup": "Sicherung",
"backup_controller_page_backup_selected": "Ausgewählt: ",
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
"backup_controller_page_cancel": "Abbrechen",
"backup_controller_page_created": "Erstellt: {}",
"backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
"backup_controller_page_excluded": "Ausgeschlossen: ",
"backup_controller_page_failed": "Fehlgeschlagen ({})",
"backup_controller_page_filename": "Dateiname: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informationen zur Sicherung",
"backup_controller_page_none_selected": "Keine ausgewählt",
"backup_controller_page_remainder": "Übrig",
"backup_controller_page_remainder_sub": "Noch zu sichernde Fotos und Videos",
"backup_controller_page_select": "Auswählen",
"backup_controller_page_server_storage": "Server Speicher",
"backup_controller_page_start_backup": "Sicherung starten",
"backup_controller_page_status_off": "Sicherung ist inaktiv",
"backup_controller_page_status_on": "Sicherung ist aktiv",
"backup_controller_page_storage_format": "{} von {} genutzt",
"backup_controller_page_to_backup": "Zu sichernde Alben",
"backup_controller_page_total": "Gesamt",
"backup_controller_page_total_sub": "Alle Fotos und Videos",
"backup_controller_page_turn_off": "Sicherung ausschalten",
"backup_controller_page_turn_on": "Sicherung einschalten",
"backup_controller_page_uploading_file_info": "Informationen",
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
"backup_info_card_assets": "Elemente",
"control_bottom_app_bar_delete": "Löschen",
"create_shared_album_page_share": "Teilen",
"create_shared_album_page_create": "Erstellen",
"create_shared_album_page_share_add_assets": "FOTOS HINZUFÜGEN",
"create_shared_album_page_share_select_photos": "Fotos auswählen",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E d. LLL y • hh:mm",
"delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt",
"delete_dialog_cancel": "Abbrechen",
"delete_dialog_ok": "Löschen",
"delete_dialog_title": "Für immer löschen",
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "STANDORT",
"login_form_button_text": "Anmelden",
"login_form_email_hint": "deine@email.de",
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
"login_form_endpoint_url": "Server URL",
"login_form_err_http": "Bitte gebe http:// oder https:// an",
"login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
"login_form_failed_login": "Fehler bei der Anmeldung, überprüfen Sie Server URL, E-Mail und Passwort",
"login_form_label_email": "E-Mail",
"login_form_label_password": "Passwort",
"login_form_password_hint": "Passwort",
"login_form_save_login": "Angemeldet bleiben",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
"profile_drawer_sign_out": "Abmelden",
"profile_drawer_settings": "Einstellungen",
"search_bar_hint": "Durchsuche deine Fotos",
"search_page_no_objects": "Keine Objektinformationen verfügbar",
"search_page_no_places": "Keine Informationen über Orte verfügbar",
"search_page_places": "Orte",
"search_page_things": "Dinge",
"search_result_page_new_search_hint": "Neue Suche",
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"select_user_for_sharing_page_share_suggestions": "Vorschläge",
"share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen",
"share_create_album": "Album erstellen",
"share_invite": "Zum Album einladen",
"sharing_page_album": "Geteilte Alben",
"sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
"sharing_page_empty_list": "LEERE LISTE",
"sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
"sharing_silver_appbar_share_partner": "Teile mit Partner",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Suche",
"tab_controller_nav_sharing": "Teilen",
"tab_controller_nav_library": "Bibliothek",
"version_announcement_overlay_ack": "Ich habe verstanden",
"version_announcement_overlay_release_notes": "Änderungsprotokoll",
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89",
"album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"library_page_albums": "Alben",
"library_page_new_album": "Neues Album",
"create_album_page_untitled": "Unbenannt",
"share_dialog_preparing": "Vorbereiten...",
"control_bottom_app_bar_share": "Teilen",
"setting_pages_app_bar_settings": "Einstellungen",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App",
"theme_setting_system_theme_switch": "Automatisch (Systemeinstellung folgen)",
"theme_setting_dark_mode_switch": "Dunkler Modus",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren",
"theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich"
}

View File

@@ -0,0 +1,153 @@
{
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_viewer_appbar_share_delete": "Delete album",
"album_viewer_appbar_share_err_delete": "Failed to delete album",
"album_viewer_appbar_share_err_leave": "Failed to leave album",
"album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
"album_viewer_appbar_share_err_title": "Failed to change album title",
"album_viewer_appbar_share_leave": "Leave album",
"album_viewer_appbar_share_remove": "Remove from album",
"album_viewer_page_share_add_users": "Add users",
"backup_album_selection_page_albums_device": "Albums on device ({})",
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
"backup_album_selection_page_select_albums": "Select Albums",
"backup_album_selection_page_selection_info": "Selection Info",
"backup_album_selection_page_total_assets": "Total unique assets",
"backup_all": "All",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_disable_battery_optimizations": "Please disable battery optimization for Immich to enable background backup",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_error_title": "Backup error",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
"backup_controller_page_albums": "Backup Albums",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selected: ",
"backup_controller_page_backup_sub": "Backed up photos and videos",
"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_wifi": "Only on WiFi",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_cancel": "Cancel",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
"backup_controller_page_excluded": "Excluded: ",
"backup_controller_page_failed": "Failed ({})",
"backup_controller_page_filename": "File name: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Backup Information",
"backup_controller_page_none_selected": "None selected",
"backup_controller_page_remainder": "Remainder",
"backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
"backup_controller_page_select": "Select",
"backup_controller_page_server_storage": "Server Storage",
"backup_controller_page_start_backup": "Start Backup",
"backup_controller_page_status_off": "Backup is off",
"backup_controller_page_status_on": "Backup is on",
"backup_controller_page_storage_format": "{} of {} used",
"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 Backup",
"backup_controller_page_turn_on": "Turn on Backup",
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "Cannot remove the only album",
"backup_info_card_assets": "assets",
"control_bottom_app_bar_delete": "Delete",
"create_shared_album_page_share": "Share",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share_add_assets": "ADD PHOTOS",
"create_shared_album_page_share_select_photos": "Select Photos",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
"delete_dialog_cancel": "Cancel",
"delete_dialog_ok": "Delete",
"delete_dialog_title": "Delete Permanently",
"exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
"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_leading_whitespace": "Leading whitespace",
"login_form_err_trailing_whitespace": "Trailing whitespace",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"login_form_label_email": "Email",
"login_form_label_password": "Password",
"login_form_password_hint": "password",
"login_form_save_login": "Stay logged in",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_sign_out": "Sign out",
"profile_drawer_settings": "Settings",
"search_bar_hint": "Search your photos",
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available",
"search_page_places": "Places",
"search_page_things": "Things",
"search_result_page_new_search_hint": "New Search",
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
"select_user_for_sharing_page_err_album": "Failed to create album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"share_add": "Add",
"share_add_photos": "Add photos",
"share_add_title": "Add a title",
"share_create_album": "Create album",
"share_invite": "Invite to album",
"sharing_page_album": "Shared albums",
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
"sharing_page_empty_list": "EMPTY LIST",
"sharing_silver_appbar_create_shared_album": "Create shared album",
"sharing_silver_appbar_share_partner": "Share with partner",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search",
"tab_controller_nav_sharing": "Sharing",
"tab_controller_nav_library": "Library",
"version_announcement_overlay_ack": "Acknowledge",
"version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"create_album_page_untitled": "Untitled",
"share_dialog_preparing": "Preparing...",
"control_bottom_app_bar_share": "Share",
"setting_pages_app_bar_settings": "Settings",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"asset_list_settings_title": "Photo Grid",
"asset_list_settings_subtitle": "Photo grid layout settings",
"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 ({})",
"setting_notifications_title": "Notifications",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_immediately": "immediately",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_never": "never"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "EXCLUIDOS",
"album_info_card_backup_album_included": "INCLUIDOS",
"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",
"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 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_controller_page_albums": "Álbumes de copia de seguridad",
"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": "",
"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": "",
"backup_controller_page_filename": "",
"backup_controller_page_id": "",
"backup_controller_page_info": "Información de la Copia de Seguridad",
"backup_controller_page_none_selected": "Ninguno seleccionado",
"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 {} 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": "",
"backup_err_only_album": "No se puede eliminar el único álbum",
"backup_info_card_assets": "activos",
"control_bottom_app_bar_delete": "Eliminar",
"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",
"daily_title_text_date": "E dd, MMM",
"daily_title_text_date_year": "E dd de MMM, yyyy",
"date_format": "E d, LLL y • h:mm a",
"delete_dialog_alert": "Estos elementos serán eliminados permanentemente de Immich y de tu dispositivo",
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Eliminar",
"delete_dialog_title": "Eliminar Permanentemente",
"exif_bottom_sheet_description": "Añadir Descripción...",
"exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "LOCALZACIÓN",
"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_leading_whitespace": "Espacio en blanco inicial",
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
"login_form_failed_login": "",
"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_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
"profile_drawer_sign_out": "Cerrar Sesión",
"search_bar_hint": "Busca tus fotos",
"search_page_no_objects": "",
"search_page_no_places": "No hay información de lugares disponibles",
"search_page_places": "Lugares",
"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": "",
"share_add": "Añadir",
"share_add_photos": "Añadir fotos",
"share_add_title": "Añadir un título",
"share_create_album": "Crear álbum",
"share_invite": "Invitar al álbum",
"sharing_page_album": "Álbumes compartidos",
"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 el compañero",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Buscar",
"tab_controller_nav_sharing": "Compartiendo",
"version_announcement_overlay_ack": "Reconocer",
"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

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "JÄTETTY POIS",
"album_info_card_backup_album_included": "SISÄLLYTETTY",
"album_viewer_appbar_share_delete": "Poista albumi",
"album_viewer_appbar_share_err_delete": "Albumin poistaminen epäonnistui",
"album_viewer_appbar_share_err_leave": "Albumista poistuminen epäonnistui",
"album_viewer_appbar_share_err_remove": "Ongelmia kohteiden poistamisessa albumista",
"album_viewer_appbar_share_err_title": "Albumin nimen muuttaminen epäonnistui",
"album_viewer_appbar_share_leave": "Poistu albumista",
"album_viewer_appbar_share_remove": "Poista albumista",
"album_viewer_page_share_add_users": "Lisää käyttäjiä",
"backup_album_selection_page_albums_device": "Laitteen albumit ({})",
"backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois",
"backup_album_selection_page_assets_scatter": "Kohteet voivat olla hajaantuneina useisiin albumeihin. Albumeita voidaan sisällyttää varmuuskopiointiin tai jättää siitä pois.",
"backup_album_selection_page_select_albums": "Valitse albumit",
"backup_album_selection_page_selection_info": "Valintatiedot",
"backup_album_selection_page_total_assets": "Uniikkeja kohteita yhteensä",
"backup_all": "Kaikki",
"backup_controller_page_albums": "Varmuuskopioi albumit",
"backup_controller_page_backup": "Varmuuskopioitu",
"backup_controller_page_backup_selected": "Valittu:",
"backup_controller_page_backup_sub": "Varmuuskopioidut kuvat ja videot",
"backup_controller_page_cancel": "Peruuta",
"backup_controller_page_created": "Luotu: {}",
"backup_controller_page_desc_backup": "Kytke varmuuskopiointi päälle ladataksesi uudet kohteet palvelimelle automaattisesti.",
"backup_controller_page_excluded": "Jätetty pois:",
"backup_controller_page_failed": "Epäonnistui ({})",
"backup_controller_page_filename": "Tiedoston nimi: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Varmuuskopioinnin tiedot",
"backup_controller_page_none_selected": "Ei mitään",
"backup_controller_page_remainder": "Jäljellä",
"backup_controller_page_remainder_sub": "Varmuuskopiointia odottavat kuvat ja videot",
"backup_controller_page_select": "Valitse",
"backup_controller_page_server_storage": "Palvelimen tallennustila",
"backup_controller_page_start_backup": "Aloita varmuuskopiointi",
"backup_controller_page_status_off": "Varmuuskopiointi on pois päältä",
"backup_controller_page_status_on": "Varmuuskopiointi on päällä",
"backup_controller_page_storage_format": "{} / {} käytetty",
"backup_controller_page_to_backup": "Varmuuskopioitavat albumit",
"backup_controller_page_total": "Yhteensä",
"backup_controller_page_total_sub": "Kaikki uniikit kuvat ja videot valituista albumeista",
"backup_controller_page_turn_off": "Varmuuskopiointi pois päältä",
"backup_controller_page_turn_on": "Varmuuskopiointi päälle",
"backup_controller_page_uploading_file_info": "Tiedostojen lähetystiedot",
"backup_err_only_album": "Vähintään yhden albumin tulee olla valittuna",
"backup_info_card_assets": "kohdetta",
"control_bottom_app_bar_delete": "Poista",
"create_shared_album_page_share": "Jaa",
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
"create_shared_album_page_share_select_photos": "Valitse kuvat",
"daily_title_text_date": "",
"daily_title_text_date_year": "",
"date_format": "",
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
"delete_dialog_cancel": "Peruuta",
"delete_dialog_ok": "Poista",
"delete_dialog_title": "Poista pysyvästi",
"exif_bottom_sheet_description": "Lisää kuvaus…",
"exif_bottom_sheet_details": "TIEDOT",
"exif_bottom_sheet_location": "SIJAINTI",
"login_form_button_text": "Kirjaudu",
"login_form_email_hint": "sahkopostisi@esimerkki.fi",
"login_form_endpoint_hint": "http://palvelimesi-osoite:portti/api",
"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_leading_whitespace": "Alussa välilyönti",
"login_form_err_trailing_whitespace": "Lopussa välilyönti",
"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": "",
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
"profile_drawer_sign_out": "Kirjaudu ulos",
"search_bar_hint": "Etsi kuvia",
"search_page_no_objects": "Objektitietoja ei ole saatavilla",
"search_page_no_places": "Paikkatietoja ei ole saatavilla",
"search_page_places": "Paikat",
"search_page_things": "Asiat",
"search_result_page_new_search_hint": "Uusi haku",
"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",
"share_add": "Lisää",
"share_add_photos": "Lisää kuvia",
"share_add_title": "Lisää nimi",
"share_create_album": "Luo albumi",
"share_invite": "Kutsu albumiin",
"sharing_page_album": "Jaetut albumit",
"sharing_page_description": "Luo jaettuja albumeja jakaaksesi kuvia ja videoita läheisillesi.",
"sharing_page_empty_list": "TYHJÄ LISTA",
"sharing_silver_appbar_create_shared_album": "Luo jaettu albumi",
"sharing_silver_appbar_share_partner": "Jaa kumppanille",
"tab_controller_nav_photos": "Kuvat",
"tab_controller_nav_search": "Haku",
"tab_controller_nav_sharing": "Jakaminen",
"version_announcement_overlay_ack": "Tiedostan",
"version_announcement_overlay_release_notes": "julkaisutiedoissa",
"version_announcement_overlay_text_1": "Hei, kaveri! Uusi palvelinversio on saatavilla sovelluksesta",
"version_announcement_overlay_text_2": "Ota hetki aikaa vieraillaksesi",
"version_announcement_overlay_text_3": "ja varmista, että käyttämäsi docker-compose ja .env-asetukset ovat ajantasalla välttyäksesi asetusongelmilta. Varsinkin jos käytät WatchToweria tai jotain muuta mekanismia päivittääksesi palvelinsovellusta automaattisesti.",
"version_announcement_overlay_title": "Uusi palvelinversio saatavilla \uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "EXCLU",
"album_info_card_backup_album_included": "INCLUS",
"album_viewer_appbar_share_delete": "Supprimer l'album",
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
"album_viewer_appbar_share_err_remove": "Il y a des problèmes pour retirer les éléments de l'album",
"album_viewer_appbar_share_err_title": "Échec de la modification du titre de l'album",
"album_viewer_appbar_share_leave": "Quitter l'album",
"album_viewer_appbar_share_remove": "Retirer de l'album",
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
"backup_album_selection_page_select_albums": "Sélectionner les albums",
"backup_album_selection_page_selection_info": "Informations sur la sélection",
"backup_album_selection_page_total_assets": "Total des éléments uniques",
"backup_all": "Tout",
"backup_controller_page_albums": "Sauvegarder les albums",
"backup_controller_page_backup": "Sauvegardé",
"backup_controller_page_backup_selected": "Sélectionné : ",
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
"backup_controller_page_cancel": "Annuler",
"backup_controller_page_created": "Créé le : {}",
"backup_controller_page_desc_backup": "Activez la sauvegarde pour envoyer automatiquement les nouveaux éléments sur le serveur.",
"backup_controller_page_excluded": "Exclus : ",
"backup_controller_page_failed": "Échec de l'opération ({})",
"backup_controller_page_filename": "Nom du fichier : {} [{}]",
"backup_controller_page_id": "ID : {}",
"backup_controller_page_info": "Informations de sauvegarde",
"backup_controller_page_none_selected": "Aucune sélection",
"backup_controller_page_remainder": "Restant",
"backup_controller_page_remainder_sub": "Photos et albums restants à sauvegarder à partir de la sélection",
"backup_controller_page_select": "Sélectionner",
"backup_controller_page_server_storage": "Stockage du serveur",
"backup_controller_page_start_backup": "Démarrer la sauvegarde",
"backup_controller_page_status_off": "La sauvegarde est désactivée",
"backup_controller_page_status_on": "La sauvegarde est activée",
"backup_controller_page_storage_format": "{} de {} utilisé",
"backup_controller_page_to_backup": "Albums à sauvegarder",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés",
"backup_controller_page_turn_off": "Désactiver la sauvegarde",
"backup_controller_page_turn_on": "Activer la sauvegarde",
"backup_controller_page_uploading_file_info": "Envoi d'informations sur le fichier",
"backup_err_only_album": "Impossible de retirer le seul album",
"backup_info_card_assets": "éléments",
"control_bottom_app_bar_delete": "Supprimer",
"create_shared_album_page_share": "Partager",
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Ces éléments seront définitivement supprimés de Immich et de votre appareil.",
"delete_dialog_cancel": "Annuler",
"delete_dialog_ok": "Supprimer",
"delete_dialog_title": "Supprimer définitivement",
"exif_bottom_sheet_description": "Ajouter une description...",
"exif_bottom_sheet_details": "DÉTAILS",
"exif_bottom_sheet_location": "LOCALISATION",
"login_form_button_text": "Connexion",
"login_form_email_hint": "votreemail@email.com",
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
"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_leading_whitespace": "Espace en début de ligne",
"login_form_err_trailing_whitespace": "Espace de fin de ligne",
"login_form_failed_login": "Erreur de connexion, vérifiez l'url du serveur, l'email et le mot de passe",
"login_form_label_email": "Email",
"login_form_label_password": "Mot de passe",
"login_form_password_hint": "mot de passe",
"login_form_save_login": "Rester connecté",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
"profile_drawer_sign_out": "Se déconnecter",
"search_bar_hint": "Rechercher vos photos",
"search_page_no_objects": "Aucune information disponible sur les objets",
"search_page_no_places": "Aucune information disponible sur la localisation",
"search_page_places": "Lieux",
"search_page_things": "Objets",
"search_result_page_new_search_hint": "Nouvelle recherche",
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"share_add": "Ajouter",
"share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre",
"share_create_album": "Créer un album",
"share_invite": "Inviter à l'album",
"sharing_page_album": "Albums partagés",
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
"sharing_page_empty_list": "LISTE VIDE",
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Recherche",
"tab_controller_nav_sharing": "Partage",
"version_announcement_overlay_ack": "Confirmer",
"version_announcement_overlay_release_notes": "notes de mise à jour",
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
"version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ",
"version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.",
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "ESCLUSI",
"album_info_card_backup_album_included": "INCLUSI",
"album_viewer_appbar_share_delete": "Elimina album ",
"album_viewer_appbar_share_err_delete": "Fallito nel cancellare l'album ",
"album_viewer_appbar_share_err_leave": "Fallito nel lasciare l'album ",
"album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ",
"album_viewer_appbar_share_err_title": "Fallito nel cambiare titolo dell'album ",
"album_viewer_appbar_share_leave": "Lascia l'album",
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
"album_viewer_page_share_add_users": "Aggiungi utenti",
"backup_album_selection_page_albums_device": "Albums nel device ({})",
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
"backup_album_selection_page_select_albums": "Seleziona gli album",
"backup_album_selection_page_selection_info": "Informazioni sulla selezione ",
"backup_album_selection_page_total_assets": "Numero totale di oggetti unici",
"backup_all": "Tutti",
"backup_controller_page_albums": "Backup album",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selezionati:",
"backup_controller_page_backup_sub": "Photo e video salvati",
"backup_controller_page_cancel": "Cancella ",
"backup_controller_page_created": "Creato il: {}",
"backup_controller_page_desc_backup": "Attiva il backup automatico per eseguire upload sul server",
"backup_controller_page_excluded": "Esclusi:",
"backup_controller_page_failed": "Falliti: ({})",
"backup_controller_page_filename": "Nome del file: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informazioni sul backup",
"backup_controller_page_none_selected": "Nessuna selezione",
"backup_controller_page_remainder": "Promemoria ",
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da salvare",
"backup_controller_page_select": "Seleziona ",
"backup_controller_page_server_storage": "Spazio nel server",
"backup_controller_page_start_backup": "Inizia backup ",
"backup_controller_page_status_off": "Backup è disattivato ",
"backup_controller_page_status_on": "Backup è attivato",
"backup_controller_page_storage_format": "{} di {} usati",
"backup_controller_page_to_backup": "Album da salvare",
"backup_controller_page_total": "Totale",
"backup_controller_page_total_sub": "Tutte le foto e i video unici salvati dagli album selezionati ",
"backup_controller_page_turn_off": "Disattiva backup",
"backup_controller_page_turn_on": "Attiva backup ",
"backup_controller_page_uploading_file_info": "Info sul file caricato",
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
"backup_info_card_assets": "Oggetti ",
"control_bottom_app_bar_delete": "Elimina",
"create_shared_album_page_share": "Condividi",
"create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
"create_shared_album_page_share_select_photos": "Seleziona foto",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, d LLL, y • hh:mm",
"delete_dialog_alert": "Questi oggetti saranno cancellati permanentemente da Immich e dal tuo device",
"delete_dialog_cancel": "Annulla",
"delete_dialog_ok": "Elimina",
"delete_dialog_title": "Cancella in modo permanente ",
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
"exif_bottom_sheet_details": "DETTAGLI",
"exif_bottom_sheet_location": "POSIZIONE",
"login_form_button_text": "Accedi",
"login_form_email_hint": "tuaemail@email.com",
"login_form_endpoint_hint": "http://tuo-ip-del-server:port/api",
"login_form_endpoint_url": "URL del Server Endpoint",
"login_form_err_http": "Per favore specificare http:// o https://",
"login_form_err_invalid_email": "Email non valida",
"login_form_err_leading_whitespace": "Spazio bianco all'inizio ",
"login_form_err_trailing_whitespace": "Spazio bianco alla fine",
"login_form_failed_login": "Errore nel login, controlla URL del server e le credenziali (email e password)",
"login_form_label_email": "Email",
"login_form_label_password": "Password",
"login_form_password_hint": "password ",
"login_form_save_login": "Rimani connesso ",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
"profile_drawer_sign_out": "Esci",
"search_bar_hint": "Cerca le tue foto",
"search_page_no_objects": "Nessuna Informazione relativa all'Oggetto Disponibile",
"search_page_no_places": "Nessun informazione sulla posizione ",
"search_page_places": "Luoghi",
"search_page_things": "Oggetti",
"search_result_page_new_search_hint": "Nuova ricerca ",
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
"select_user_for_sharing_page_err_album": "Fallito nel creare l'album ",
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
"share_add": "Aggiungi",
"share_add_photos": "Aggiungi foto",
"share_add_title": "Aggiungi un titolo ",
"share_create_album": "Crea album",
"share_invite": "Invitare all'album ",
"sharing_page_album": "Album condivisi",
"sharing_page_description": "Crea un album condiviso per condividere foto e video con gente nel tuo network",
"sharing_page_empty_list": "LISTA VUOTA",
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
"sharing_silver_appbar_share_partner": "Condividi con il partner",
"tab_controller_nav_photos": "Foto",
"tab_controller_nav_search": "Cerca",
"tab_controller_nav_sharing": "Condividi",
"version_announcement_overlay_ack": "Riconosci ",
"version_announcement_overlay_release_notes": "le note di rilascio ",
"version_announcement_overlay_text_1": "Ciao amico, c'è una nuova versione di",
"version_announcement_overlay_text_2": "prova a controllare ",
"version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore nella configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico delle immagini docker.",
"version_announcement_overlay_title": "Nuova versione di server disponibile! \uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "除外",
"album_info_card_backup_album_included": "選択",
"album_viewer_appbar_share_delete": "アルバムを削除",
"album_viewer_appbar_share_err_delete": "削除に失敗...",
"album_viewer_appbar_share_err_leave": "退会に失敗...",
"album_viewer_appbar_share_err_remove": "アルバムから写真を除外する際にエラー発生",
"album_viewer_appbar_share_err_title": "タイトルの変更に失敗...",
"album_viewer_appbar_share_leave": "アルバムから退会",
"album_viewer_appbar_share_remove": "アルバムから除外",
"album_viewer_page_share_add_users": "ユーザーを追加",
"backup_album_selection_page_albums_device": "端末上のアルバム数は {} だよ",
"backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外だよ",
"backup_album_selection_page_assets_scatter": "写真がいろんなアルバムに登録されてる事があるから、アルバムを含めたり除外したりしてどの写真を保存するか選択できるよ。",
"backup_album_selection_page_select_albums": "アルバムを選択",
"backup_album_selection_page_selection_info": "選択、又は除外されてるアルバム",
"backup_album_selection_page_total_assets": "選択されたアルバムの写真と動画の数",
"backup_all": "全て",
"backup_controller_page_albums": "アルバム",
"backup_controller_page_backup": "バックアップ",
"backup_controller_page_backup_selected": "選択されてる:",
"backup_controller_page_backup_sub": "バックアップされた写真と動画の数だよ",
"backup_controller_page_cancel": "キャンセルするよ",
"backup_controller_page_created": "{} に作成されたよ",
"backup_controller_page_desc_backup": "ONにすれば自動的に新しい写真などがバックアップされるようになるよ",
"backup_controller_page_excluded": "除外されてるアルバム:",
"backup_controller_page_failed": "失敗: ({})",
"backup_controller_page_filename": "ファイル名: {} [{}] ",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "バックアップ情報",
"backup_controller_page_none_selected": "何も選んでないよ",
"backup_controller_page_remainder": "リマインダー",
"backup_controller_page_remainder_sub": "残りの写真と動画の数だよ",
"backup_controller_page_select": "選択",
"backup_controller_page_server_storage": "サーバーの容量",
"backup_controller_page_start_backup": "バックアップを開始するよ",
"backup_controller_page_status_off": "バックアップがOFFになってるよ",
"backup_controller_page_status_on": "バックアップがONになってるよ",
"backup_controller_page_storage_format": "{}中、 {}を使用中だよ",
"backup_controller_page_to_backup": "バックアップされるアルバム",
"backup_controller_page_total": "トータル",
"backup_controller_page_total_sub": "選択されたアルバムの写真と動画の数だよ",
"backup_controller_page_turn_off": "バックアップOFF",
"backup_controller_page_turn_on": "バックアップON",
"backup_controller_page_uploading_file_info": "アップロードされてるファイルに関する情報",
"backup_err_only_album": "唯一のアルバムを除外する事はできないよ",
"backup_info_card_assets": "写真と動画",
"control_bottom_app_bar_delete": "削除",
"create_shared_album_page_share": "共有",
"create_shared_album_page_share_add_assets": "写真や動画を追加",
"create_shared_album_page_share_select_photos": "写真を選択",
"daily_title_text_date": "E, MM月 dd日",
"daily_title_text_date_year": "E, yyyy年 MM月 dd日",
"date_format": "E, MM月 dd日 • hh時mm分",
"delete_dialog_alert": "サーバーからも端末からも永久的に削除されるけど良いの?",
"delete_dialog_cancel": "キャンセル",
"delete_dialog_ok": "削除",
"delete_dialog_title": "永久的に削除",
"exif_bottom_sheet_description": "概要を追加",
"exif_bottom_sheet_details": "詳細な情報",
"exif_bottom_sheet_location": "撮影地",
"login_form_button_text": "ログイン",
"login_form_email_hint": "example@email.com",
"login_form_endpoint_hint": "https://example.com:port/api",
"login_form_endpoint_url": "サーバーエンドポイントURL",
"login_form_err_http": "http://かhttps://かを指定してね",
"login_form_err_invalid_email": "メールアドレスが有効じゃないよ",
"login_form_err_leading_whitespace": "最初に半角スペースが含まれてるよ",
"login_form_err_trailing_whitespace": "最後に半角スペースが含まれてるよ",
"login_form_failed_login": "ログインエラー。サーバーのURL、メールアドレスとパスワードを再確認してね",
"login_form_label_email": "メールアドレス",
"login_form_label_password": "パスワード",
"login_form_password_hint": "パスワード",
"login_form_save_login": "ログインしたままにする",
"monthly_title_text_date_format": "yyyy年 MM月",
"profile_drawer_client_server_up_to_date": "サーバーとクライアント、両方最新バージョンだよ",
"profile_drawer_sign_out": "サインアウト",
"search_bar_hint": "写真を検索",
"search_page_no_objects": "被写体に関するデータがないよ",
"search_page_no_places": "場所に関するデータがないよ",
"search_page_places": "撮影地",
"search_page_things": "カテゴリ",
"search_result_page_new_search_hint": "検索",
"select_additional_user_for_sharing_page_suggestions": "ユーザーリスト",
"select_user_for_sharing_page_err_album": "アルバム作成に失敗...",
"select_user_for_sharing_page_share_suggestions": "ユーザーの一覧だよ",
"share_add": "追加",
"share_add_photos": "写真を追加",
"share_add_title": "タイトルを追加",
"share_create_album": "アルバムを作成",
"share_invite": "アルバムに参加",
"sharing_page_album": "共有アルバム",
"sharing_page_description": "共有アルバムを作成して同じネットワークにいる仲間に写真を共有してみよう!",
"sharing_page_empty_list": "誰も居ないね ( T_T)(^-^ ) ドンマイ",
"sharing_silver_appbar_create_shared_album": "共有アルバムを作成",
"sharing_silver_appbar_share_partner": "パートナーと共有",
"tab_controller_nav_photos": "写真",
"tab_controller_nav_search": "検索",
"tab_controller_nav_sharing": "共有",
"version_announcement_overlay_ack": "了解",
"version_announcement_overlay_release_notes": "更新情報",
"version_announcement_overlay_text_1": "こんにちは、又はこんばんは!新しい",
"version_announcement_overlay_text_2": "のバージョンが公開中だよ。",
"version_announcement_overlay_text_3": "を確認してみてね。あと、docker-composeや.envファイルが最新の状態に更新されてか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートしてる人は確認してね",
"version_announcement_overlay_title": "新しいバージョン、公開中\uD83C\uDF89"
}

View File

@@ -0,0 +1,106 @@
{
"album_info_card_backup_album_excluded": "WYKLUCZONE",
"album_info_card_backup_album_included": "WŁĄCZONE",
"album_viewer_appbar_share_delete": "Usuń album",
"album_viewer_appbar_share_err_delete": "Nie udało się usunąć albumu",
"album_viewer_appbar_share_err_leave": "Nie udało się wyjść z albumu",
"album_viewer_appbar_share_err_remove": "Wystąpiły problemy z usunięciem plików z albumu",
"album_viewer_appbar_share_err_title": "Nie udało się zmienić tytułu albumu",
"album_viewer_appbar_share_leave": "Opuść album",
"album_viewer_appbar_share_remove": "Usuń z albumu",
"album_viewer_page_share_add_users": "Dodaj użytkowników",
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})",
"backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć",
"backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.",
"backup_album_selection_page_select_albums": "Zaznacz albumy",
"backup_album_selection_page_selection_info": "Info o wyborze",
"backup_album_selection_page_total_assets": "Łącznie unikalnych plików",
"backup_all": "Wszystkie",
"backup_controller_page_albums": "Backup Albumów",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Zaznaczone: ",
"backup_controller_page_backup_sub": "Tworzenie kopii zapasowych zdjęć i filmów",
"backup_controller_page_cancel": "Anuluj",
"backup_controller_page_created": "Utworzony na: {}",
"backup_controller_page_desc_backup": "Włącz backup, aby automatycznie przesyłać nowe zasoby na serwer.",
"backup_controller_page_excluded": "Wykluczone: ",
"backup_controller_page_failed": "Nieudane ({})",
"backup_controller_page_filename": "Nazwa pliku: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informacje o kopii zapasowej",
"backup_controller_page_none_selected": "Brak wybranych",
"backup_controller_page_remainder": "Reszta",
"backup_controller_page_remainder_sub": "Pozostałe zdjęcia i albumy do wykonania kopii zapasowej z wyboru",
"backup_controller_page_select": "Zaznacz",
"backup_controller_page_server_storage": "Pamięć Serwera",
"backup_controller_page_start_backup": "Rozpocznij Backup",
"backup_controller_page_status_off": "Backup jest wyłączony",
"backup_controller_page_status_on": "Backup jest włączony",
"backup_controller_page_storage_format": "{} z {} wykorzystanych",
"backup_controller_page_to_backup": "Albumy do backupu",
"backup_controller_page_total": "Łącznie",
"backup_controller_page_total_sub": "Wszystkie unikalne zdjęcia i filmy z wybranych albumów",
"backup_controller_page_turn_off": "Wyłącz Backup",
"backup_controller_page_turn_on": "Włącz Backup",
"backup_controller_page_uploading_file_info": "Przesyłanie informacji o pliku",
"backup_err_only_album": "Nie można usunąć tylko i wyłącznie albumu",
"backup_info_card_assets": "pliki",
"control_bottom_app_bar_delete": "Usuń",
"create_shared_album_page_share": "Udostępnij",
"create_shared_album_page_share_add_assets": "DODAJ PLIKI",
"create_shared_album_page_share_select_photos": "Zaznacz Zdjęcia",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Te elementy zostaną trwale usunięte z Immich i z Twojego urządzenia",
"delete_dialog_cancel": "Anuluj",
"delete_dialog_ok": "Usuń",
"delete_dialog_title": "Usuń trwale",
"exif_bottom_sheet_description": "Dodaj opis...",
"exif_bottom_sheet_details": "SZCZEGÓŁY",
"exif_bottom_sheet_location": "LOKALIZACJA",
"login_form_button_text": "Login",
"login_form_email_hint": "twojmail@email.com",
"login_form_endpoint_hint": "http://ip-twojego-serwera:port/api",
"login_form_endpoint_url": "URL Serwera",
"login_form_err_http": "Proszę określić http:// lub https://",
"login_form_err_invalid_email": "Niepoprawny emaill",
"login_form_err_leading_whitespace": "Białe znaki",
"login_form_err_trailing_whitespace": "Białe znaki po przecinku",
"login_form_failed_login": "Błąd logowania, sprawdź adres url serwera, email i hasło.",
"login_form_label_email": "Email",
"login_form_label_password": "Hasło",
"login_form_password_hint": "hasło",
"login_form_save_login": "Pozostań zalogowany",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne",
"profile_drawer_sign_out": "Wyloguj się",
"search_bar_hint": "Szukaj swoich zdjęć",
"search_page_no_objects": "Brak informacji o obiektach",
"search_page_no_places": "Brak informacji o miejscu",
"search_page_places": "Miejsca",
"search_page_things": "Rzeczy",
"search_result_page_new_search_hint": "Nowe wyszukiwanie",
"select_additional_user_for_sharing_page_suggestions": "Propozycje",
"select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
"select_user_for_sharing_page_share_suggestions": "Propozycje",
"share_add": "Dodaj",
"share_add_photos": "Dodaj zdjęcia",
"share_add_title": "Dodaj tytuł",
"share_create_album": "Utwórz album",
"share_invite": "Zaproś do albumu",
"sharing_page_album": "Udostępnione albumy",
"sharing_page_description": "Twórz wspóldzielone albumy, aby udostępniać zdjęcia i filmy osobom w sieci.",
"sharing_page_empty_list": "PUSTA LISTA",
"sharing_silver_appbar_create_shared_album": "Utwórz współdzielony album",
"sharing_silver_appbar_share_partner": "Udostępnij partnerce/partnerowi",
"tab_controller_nav_photos": "Zdjęcia",
"tab_controller_nav_search": "Szukaj",
"tab_controller_nav_sharing": "Udostępnianie",
"version_announcement_overlay_ack": "Potwierdzenie",
"version_announcement_overlay_release_notes": "informacje o wydaniu",
"version_announcement_overlay_text_1": "Cześć przyjacielu, jest nowe wydanie",
"version_announcement_overlay_text_2": "prosimy o poświęcenie czasu na odwiedzenie ",
"version_announcement_overlay_text_3": " i upewnij się, że twoja konfiguracja docker-compose i .env jest aktualna, aby zapobiec błędnym konfiguracjom, zwłaszcza jeśli używasz WatchTower lub dowolnego mechanizmu, który obsługuje automatyczną aktualizację aplikacji serwera.",
"version_announcement_overlay_title": "Nowa wersja serwera dostępna \uD83C\uDF89"
}

View File

@@ -0,0 +1,140 @@
{
"album_info_card_backup_album_excluded": "EXCLUÍDO",
"album_info_card_backup_album_included": "INCLUÍDO",
"album_viewer_appbar_share_delete": "Excluir álbum",
"album_viewer_appbar_share_err_delete": "Falha ao excluir álbum",
"album_viewer_appbar_share_err_leave": "Falha ao sair do álbum",
"album_viewer_appbar_share_err_remove": "Há problemas ao remover recursos do álbum",
"album_viewer_appbar_share_err_title": "Falha ao alterar o título do álbum",
"album_viewer_appbar_share_leave": "Sair do álbum",
"album_viewer_appbar_share_remove": "Remover do álbum",
"album_viewer_page_share_add_users": "Adicionar usuários",
"backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})",
"backup_album_selection_page_albums_tap": "Toque para incluir, toque duas vezes para excluir",
"backup_album_selection_page_assets_scatter": "Os recursos podem se espalhar por vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.",
"backup_album_selection_page_select_albums": "Selecionar álbuns",
"backup_album_selection_page_selection_info": "Informações da Seleção",
"backup_album_selection_page_total_assets": "Total de recursos exclusivos",
"backup_all": "Todos",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_disable_battery_optimizations": "Por favor, desabilite a otimização da bateria para Immich para habilitar o backup em segundo plano",
"backup_background_service_upload_failure_notification": "Falha ao carregar {}",
"backup_background_service_in_progress_notification": "Fazendo backup de seus ativos…",
"backup_background_service_current_upload_notification": "Enviando {}",
"backup_controller_page_albums": "Álbuns de backup",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selecionado: ",
"backup_controller_page_backup_sub": "Backup de fotos e vídeos",
"backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automático de novos ativos sem precisar abrir o aplicativo",
"backup_controller_page_background_wifi": "Apenas em Wi-Fi",
"backup_controller_page_background_charging": "Apenas durante o carregamento",
"backup_controller_page_background_is_on": "O backup automático em segundo plano está ativado",
"backup_controller_page_background_is_off": "O backup automático em segundo plano está desativado",
"backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano",
"backup_controller_page_background_turn_off": "Desativar o serviço em segundo plano",
"backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano",
"backup_controller_page_cancel": "Cancelar",
"backup_controller_page_created": "Criado em: {}",
"backup_controller_page_desc_backup": "Ative o backup para carregar automaticamente novos ativos no servidor.",
"backup_controller_page_excluded": "Excluído: ",
"backup_controller_page_failed": "Falhou ({})",
"backup_controller_page_filename": "Nome do arquivo: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informações de backup",
"backup_controller_page_none_selected": "Nenhum selecionado",
"backup_controller_page_remainder": "Restante",
"backup_controller_page_remainder_sub": "Fotos e vídeos restantes para fazer backup da seleção",
"backup_controller_page_select": "Selecionar",
"backup_controller_page_server_storage": "Armazenamento do servidor",
"backup_controller_page_start_backup": "Iniciar backup",
"backup_controller_page_status_off": "O backup está desativado",
"backup_controller_page_status_on": "O backup está ativado",
"backup_controller_page_storage_format": "{} de {} usado",
"backup_controller_page_to_backup": "Álbuns para backup",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Todas as fotos e vídeos únicos dos álbuns selecionados",
"backup_controller_page_turn_off": "Desativar o backup",
"backup_controller_page_turn_on": "Ativar Backup",
"backup_controller_page_uploading_file_info": "Carregando informações do arquivo",
"backup_err_only_album": "Não é possível remover o único álbum",
"backup_info_card_assets": "ativos",
"control_bottom_app_bar_delete": "Excluir",
"create_shared_album_page_share": "Compartilhar",
"create_shared_album_page_create": "Criar",
"create_shared_album_page_share_add_assets": "ADICIONAR FOTOS",
"create_shared_album_page_share_select_photos": "Selecionar fotos",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Esses itens serão excluídos permanentemente do Immich e do seu dispositivo",
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Excluir",
"delete_dialog_title": "Excluir permanentemente",
"exif_bottom_sheet_description": "Adicionar descrição...",
"exif_bottom_sheet_details": "DETALHES",
"exif_bottom_sheet_location": "LOCALIZAÇÃO",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Please specify http:// or https://",
"login_form_err_invalid_email": "E-mail inválido",
"login_form_err_leading_whitespace": "Leading whitespace",
"login_form_err_trailing_whitespace": "Trailing whitespace",
"login_form_failed_login": "Erro ao fazer login, verifique a url do servidor, e-mail e senha",
"login_form_label_email": "Email",
"login_form_label_password": "Password",
"login_form_password_hint": "password",
"login_form_save_login": "Permaneçer conectado",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Cliente e Servidor estão atualizados",
"profile_drawer_sign_out": "Sair",
"profile_drawer_settings": "Configurações",
"search_bar_hint": "Procurar fotos",
"search_page_no_objects": "Nenhuma informação de objeto disponível",
"search_page_no_places": "Nenhuma informação de lugares disponível",
"search_page_places": "Lugares",
"search_page_things": "Coisas",
"search_result_page_new_search_hint": "Nova pesquisa",
"select_additional_user_for_sharing_page_suggestions": "Sugestões",
"select_user_for_sharing_page_err_album": "Falha ao criar álbum",
"select_user_for_sharing_page_share_suggestions": "Sugestões",
"share_add": "Adicionar",
"share_add_photos": "Adicionar fotos",
"share_add_title": "Adicione um título",
"share_create_album": "Criar álbum",
"share_invite": "Convidar para o álbum",
"sharing_page_album": "Álbuns compartilhados",
"sharing_page_description": "Crie álbuns compartilhados para compartilhar fotos e vídeos com pessoas em sua rede.",
"sharing_page_empty_list": "LISTA VAZIA",
"sharing_silver_appbar_create_shared_album": "Criar álbum compartilhado",
"sharing_silver_appbar_share_partner": "Compartilhe com o parceiro",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Procurar",
"tab_controller_nav_sharing": "Compartilhamento",
"tab_controller_nav_library": "Biblioteca",
"version_announcement_overlay_ack": "Confirmar",
"version_announcement_overlay_release_notes": "notas de lançamento",
"version_announcement_overlay_text_1": "Oi amigo, há um novo lançamento de",
"version_announcement_overlay_text_2": "reserve um tempo para visitar o ",
"version_announcement_overlay_text_3": " e verifique se a configuração do docker-compose e do .env está atualizada para evitar configurações incorretas, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do aplicativo do servidor.",
"version_announcement_overlay_title": "Nova versão do servidor disponível \uD83C\uDF89",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Compartilhado",
"library_page_albums": "Albums",
"library_page_new_album": "Novo album",
"create_album_page_untitled": "Sem título",
"share_dialog_preparing": "Preparando...",
"control_bottom_app_bar_share": "Compartilhar",
"setting_pages_app_bar_settings": "Configurações",
"theme_setting_theme_title": "Tema",
"theme_setting_theme_subtitle": "Escolha a configuração de tema do app",
"theme_setting_system_theme_switch": "Automático (seguir a configuração do sistema)",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_title": "Qualidade das imagens do visualizador",
"theme_setting_image_viewer_quality_subtitle": "Ajuste a qualidade de imagens detalhadas do visualizador",
"theme_setting_three_stage_loading_title": "Ative o carregamento em três estágios",
"theme_setting_three_stage_loading_subtitle": "O carregamento em três estágios oferece a imagem de melhor qualidade em troca de uma velocidade de carregamento mais lenta"
}

View File

@@ -19,6 +19,10 @@ PODS:
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
- share_plus (0.0.1):
- Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
@@ -38,6 +42,8 @@ DEPENDENCIES:
- 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`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
@@ -64,6 +70,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_ios/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
@@ -83,6 +93,8 @@ SPEC CHECKSUMS:
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de

View File

@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14;
CURRENT_PROJECT_VERSION = 40;
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 = 14;
CURRENT_PROJECT_VERSION = 40;
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 = 14;
CURRENT_PROJECT_VERSION = 40;
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.10.0</string>
<string>1.21.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>14</string>
<string>40</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@@ -66,8 +66,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true />
<key>io.flutter.embedded_views_preview</key>
@@ -82,5 +81,18 @@
<array>
<string>https</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>de</string>
<string>da</string>
<string>es</string>
<string>fr</string>
<string>it</string>
<string>fi</string>
<string>ja</string>
<string>pl</string>
</array>
</dict>
</plist>

View File

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

View File

@@ -5,12 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000946">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000205">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="16.3225">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.360401">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.012696">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.378836">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="80.023705">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="98.18403">
</testcase>

View File

@@ -16,3 +16,10 @@ const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1
// Github Release Info
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
// User Setting Info
const String userSettingInfoBox = "immichUserSettingInfoBox";
// Background backup Info
const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
const String backupFailedSince = "immichBackupFailedSince"; // Key 1

View File

@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
const immichBackgroundColor = Color(0xFFf6f8fe);
Color immichBackgroundColor = const Color(0xFFf6f8fe);
Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0);
Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250);

View File

@@ -0,0 +1,18 @@
import 'dart:ui';
const List<Locale> locales = [
// Default locale
Locale('en', 'US'),
// Additional locales
Locale('da', 'DK'),
Locale('de', 'DE'),
Locale('es', 'ES'),
Locale('fi', 'FI'),
Locale('fr', 'FR'),
Locale('it', 'IT'),
Locale('ja', 'JP'),
Locale('pl', 'PL'),
Locale('pt', 'PR')
];
const String translationsPath = 'assets/i18n';

View File

@@ -1,8 +1,15 @@
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';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
@@ -16,7 +23,7 @@ import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'constants/hive_box.dart';
void main() async {
@@ -29,6 +36,7 @@ void main() async {
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox);
await Hive.openBox(userSettingInfoBox);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
@@ -36,7 +44,25 @@ void main() async {
),
);
runApp(const ProviderScope(child: ImmichApp()));
await EasyLocalization.ensureInitialized();
if (kReleaseMode && Platform.isAndroid) {
try {
await FlutterDisplayMode.setHighRefreshRate();
} catch (e) {
debugPrint("Error setting high refresh rate: $e");
}
}
runApp(
EasyLocalization(
supportedLocales: locales,
path: translationsPath,
useFallbackTranslations: true,
fallbackLocale: locales.first,
child: const ProviderScope(child: ImmichApp()),
),
);
}
class ImmichApp extends ConsumerStatefulWidget {
@@ -58,6 +84,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
if (isAuthenticated) {
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
ref.watch(backupProvider.notifier).resumeBackup();
ref.watch(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion();
@@ -97,6 +124,10 @@ class ImmichAppState extends ConsumerState<ImmichApp>
initState() {
super.initState();
initApp().then((_) => debugPrint("App Init Completed"));
WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
});
}
@override
@@ -105,38 +136,28 @@ class ImmichAppState extends ConsumerState<ImmichApp>
super.dispose();
}
final _immichRouter = AppRouter();
@override
Widget build(BuildContext context) {
var router = ref.watch(appRouterProvider);
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
return MaterialApp(
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
home: Stack(
children: [
MaterialApp.router(
title: 'Immich',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
fontFamily: 'WorkSans',
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
scaffoldBackgroundColor: immichBackgroundColor,
appBarTheme: const AppBarTheme(
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 1,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
themeMode: ref.watch(immichThemeProvider),
darkTheme: immichDarkTheme,
theme: immichLightTheme,
routeInformationParser: router.defaultRouteParser(),
routerDelegate: router.delegate(
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
),
routeInformationParser: _immichRouter.defaultRouteParser(),
routerDelegate: _immichRouter.delegate(
navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
),
const ImmichLoadingOverlay(),
const VersionAnnouncementOverlay(),

View File

@@ -1,12 +1,10 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:openapi/api.dart';
class AssetSelectionPageResult {
final Set<ImmichAsset> selectedNewAsset;
final Set<ImmichAsset> selectedAdditionalAsset;
final Set<AssetResponseDto> selectedNewAsset;
final Set<AssetResponseDto> selectedAdditionalAsset;
final bool isAlbumExist;
AssetSelectionPageResult({
@@ -16,8 +14,8 @@ class AssetSelectionPageResult {
});
AssetSelectionPageResult copyWith({
Set<ImmichAsset>? selectedNewAsset,
Set<ImmichAsset>? selectedAdditionalAsset,
Set<AssetResponseDto>? selectedNewAsset,
Set<AssetResponseDto>? selectedAdditionalAsset,
bool? isAlbumExist,
}) {
return AssetSelectionPageResult(
@@ -28,35 +26,6 @@ class AssetSelectionPageResult {
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll(
{'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
result.addAll({
'selectedAdditionalAsset':
selectedAdditionalAsset.map((x) => x.toMap()).toList()
});
result.addAll({'isAlbumExist': isAlbumExist});
return result;
}
factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
return AssetSelectionPageResult(
selectedNewAsset: Set<ImmichAsset>.from(
map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
selectedAdditionalAsset: Set<ImmichAsset>.from(
map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
isAlbumExist: map['isAlbumExist'] ?? false,
);
}
String toJson() => json.encode(toMap());
factory AssetSelectionPageResult.fromJson(String source) =>
AssetSelectionPageResult.fromMap(json.decode(source));
@override
String toString() =>
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';

View File

@@ -1,14 +1,12 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:openapi/api.dart';
class AssetSelectionState {
final Set<String> selectedMonths;
final Set<ImmichAsset> selectedNewAssetsForAlbum;
final Set<ImmichAsset> selectedAdditionalAssetsForAlbum;
final Set<ImmichAsset> selectedAssetsInAlbumViewer;
final Set<AssetResponseDto> selectedNewAssetsForAlbum;
final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
final bool isMultiselectEnable;
/// Indicate the asset selection page is navigated from existing album
@@ -24,9 +22,9 @@ class AssetSelectionState {
AssetSelectionState copyWith({
Set<String>? selectedMonths,
Set<ImmichAsset>? selectedNewAssetsForAlbum,
Set<ImmichAsset>? selectedAdditionalAssetsForAlbum,
Set<ImmichAsset>? selectedAssetsInAlbumViewer,
Set<AssetResponseDto>? selectedNewAssetsForAlbum,
Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
bool? isMultiselectEnable,
bool? isAlbumExist,
}) {
@@ -43,49 +41,6 @@ class AssetSelectionState {
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'selectedMonths': selectedMonths.toList()});
result.addAll({
'selectedNewAssetsForAlbum':
selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()
});
result.addAll({
'selectedAdditionalAssetsForAlbum':
selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()
});
result.addAll({
'selectedAssetsInAlbumViewer':
selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()
});
result.addAll({'isMultiselectEnable': isMultiselectEnable});
result.addAll({'isAlbumExist': isAlbumExist});
return result;
}
factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
return AssetSelectionState(
selectedMonths: Set<String>.from(map['selectedMonths']),
selectedNewAssetsForAlbum: Set<ImmichAsset>.from(
map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
selectedAdditionalAssetsForAlbum: Set<ImmichAsset>.from(
map['selectedAdditionalAssetsForAlbum']
?.map((x) => ImmichAsset.fromMap(x))),
selectedAssetsInAlbumViewer: Set<ImmichAsset>.from(
map['selectedAssetsInAlbumViewer']
?.map((x) => ImmichAsset.fromMap(x))),
isMultiselectEnable: map['isMultiselectEnable'] ?? false,
isAlbumExist: map['isAlbumExist'] ?? false,
);
}
String toJson() => json.encode(toMap());
factory AssetSelectionState.fromJson(String source) =>
AssetSelectionState.fromMap(json.decode(source));
@override
String toString() {
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
@@ -99,10 +54,14 @@ class AssetSelectionState {
return other is AssetSelectionState &&
setEquals(other.selectedMonths, selectedMonths) &&
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
setEquals(other.selectedAdditionalAssetsForAlbum,
selectedAdditionalAssetsForAlbum) &&
setEquals(
other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
other.selectedAdditionalAssetsForAlbum,
selectedAdditionalAssetsForAlbum,
) &&
setEquals(
other.selectedAssetsInAlbumViewer,
selectedAssetsInAlbumViewer,
) &&
other.isMultiselectEnable == isMultiselectEnable &&
other.isAlbumExist == isAlbumExist;
}

View File

@@ -0,0 +1,40 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:openapi/api.dart';
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
AlbumNotifier(this._albumService) : super([]);
final AlbumService _albumService;
getAllAlbums() async {
List<AlbumResponseDto>? albums =
await _albumService.getAlbums(isShared: false);
if (albums != null) {
state = albums;
}
}
deleteAlbum(String albumId) {
state = state.where((album) => album.id != albumId).toList();
}
Future<AlbumResponseDto?> createAlbum(
String albumTitle,
Set<AssetResponseDto> assets,
) async {
AlbumResponseDto? album =
await _albumService.createAlbum(albumTitle, assets, []);
if (album != null) {
state = [...state, album];
return album;
}
return null;
}
}
final albumProvider =
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
return AlbumNotifier(ref.watch(albumServiceProvider));
});

View File

@@ -13,4 +13,5 @@ class AlbumTitleNotifier extends StateNotifier<String> {
}
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
(ref) => AlbumTitleNotifier());
(ref) => AlbumTitleNotifier(),
);

View File

@@ -1,7 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/models/album_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/modules/album/models/album_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref)
@@ -30,8 +30,11 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
}
Future<bool> changeAlbumTitle(
String albumId, String ownerId, String newAlbumTitle) async {
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
String albumId,
String ownerId,
String newAlbumTitle,
) async {
AlbumService service = ref.watch(albumServiceProvider);
bool isSuccess =
await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);

View File

@@ -1,41 +1,46 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_state.model.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:openapi/api.dart';
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
AssetSelectionNotifier()
: super(AssetSelectionState(
selectedNewAssetsForAlbum: {},
selectedMonths: {},
selectedAdditionalAssetsForAlbum: {},
selectedAssetsInAlbumViewer: {},
isAlbumExist: false,
isMultiselectEnable: false,
));
: super(
AssetSelectionState(
selectedNewAssetsForAlbum: {},
selectedMonths: {},
selectedAdditionalAssetsForAlbum: {},
selectedAssetsInAlbumViewer: {},
isAlbumExist: false,
isMultiselectEnable: false,
),
);
void setIsAlbumExist(bool isAlbumExist) {
state = state.copyWith(isAlbumExist: isAlbumExist);
}
void removeAssetsInMonth(
String removedMonth, List<ImmichAsset> assetsInMonth) {
Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
String removedMonth,
List<AssetResponseDto> assetsInMonth,
) {
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
Set<String> currentMonthList = state.selectedMonths;
currentMonthList
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
for (ImmichAsset asset in assetsInMonth) {
for (AssetResponseDto asset in assetsInMonth) {
currentAssetList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(
selectedNewAssetsForAlbum: currentAssetList,
selectedMonths: currentMonthList);
selectedNewAssetsForAlbum: currentAssetList,
selectedMonths: currentMonthList,
);
}
void addAdditionalAssets(List<ImmichAsset> assets) {
void addAdditionalAssets(List<AssetResponseDto> assets) {
state = state.copyWith(
selectedAdditionalAssetsForAlbum: {
...state.selectedAdditionalAssetsForAlbum,
@@ -44,7 +49,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
);
}
void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
state = state.copyWith(
selectedMonths: {...state.selectedMonths, month},
selectedNewAssetsForAlbum: {
@@ -54,7 +59,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
);
}
void addNewAssets(List<ImmichAsset> assets) {
void addNewAssets(List<AssetResponseDto> assets) {
state = state.copyWith(
selectedNewAssetsForAlbum: {
...state.selectedNewAssetsForAlbum,
@@ -63,20 +68,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
);
}
void removeSelectedNewAssets(List<ImmichAsset> assets) {
Set<ImmichAsset> currentList = state.selectedNewAssetsForAlbum;
void removeSelectedNewAssets(List<AssetResponseDto> assets) {
Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
for (ImmichAsset asset in assets) {
for (AssetResponseDto asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
}
void removeSelectedAdditionalAssets(List<ImmichAsset> assets) {
Set<ImmichAsset> currentList = state.selectedAdditionalAssetsForAlbum;
void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
for (ImmichAsset asset in assets) {
for (AssetResponseDto asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}
@@ -104,7 +109,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
);
}
void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
state = state.copyWith(
selectedAssetsInAlbumViewer: {
...state.selectedAssetsInAlbumViewer,
@@ -113,10 +118,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
);
}
void removeAssetsInAlbumViewer(List<ImmichAsset> assets) {
Set<ImmichAsset> currentList = state.selectedAssetsInAlbumViewer;
void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
for (ImmichAsset asset in assets) {
for (AssetResponseDto asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:openapi/api.dart';
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
final AlbumService _sharedAlbumService;
Future<AlbumResponseDto?> createSharedAlbum(
String albumName,
Set<AssetResponseDto> assets,
List<String> sharedUserIds,
) async {
try {
var newAlbum = await _sharedAlbumService.createAlbum(
albumName,
assets,
sharedUserIds,
);
if (newAlbum != null) {
state = [...state, newAlbum];
}
return newAlbum;
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
return null;
}
}
getAllSharedAlbums() async {
List<AlbumResponseDto>? sharedAlbums =
await _sharedAlbumService.getAlbums(isShared: true);
if (sharedAlbums != null) {
state = sharedAlbums;
}
}
deleteAlbum(String albumId) async {
state = state.where((album) => album.id != albumId).toList();
}
Future<bool> leaveAlbum(String albumId) async {
var res = await _sharedAlbumService.leaveAlbum(albumId);
if (res) {
state = state.where((album) => album.id != albumId).toList();
return true;
} else {
return false;
}
}
Future<bool> removeAssetFromAlbum(
String albumId,
List<String> assetIds,
) async {
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
if (res) {
return true;
} else {
return false;
}
}
}
final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
return SharedAlbumNotifier(ref.watch(albumServiceProvider));
});
final sharedAlbumDetailProvider = FutureProvider.autoDispose
.family<AlbumResponseDto?, String>((ref, albumId) async {
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
return await sharedAlbumService.getAlbumDetail(albumId);
});

View File

@@ -1,10 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/user.model.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
import 'package:openapi/api.dart';
final suggestedSharedUsersProvider =
FutureProvider.autoDispose<List<User>>((ref) async {
FutureProvider.autoDispose<List<UserResponseDto>>((ref) async {
UserService userService = ref.watch(userServiceProvider);
return await userService.getAllUsersInfo();
return await userService.getAllUsersInfo(isAll: false) ?? [];
});

View File

@@ -0,0 +1,148 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
final albumServiceProvider = Provider(
(ref) => AlbumService(
ref.watch(apiServiceProvider),
),
);
class AlbumService {
final ApiService _apiService;
AlbumService(this._apiService);
Future<List<AlbumResponseDto>?> getAlbums({required bool isShared}) async {
try {
return await _apiService.albumApi
.getAllAlbums(shared: isShared ? isShared : null);
} catch (e) {
debugPrint("Error getAllSharedAlbum ${e.toString()}");
return null;
}
}
Future<AlbumResponseDto?> createAlbum(
String albumName,
Set<AssetResponseDto> assets,
List<String> sharedUserIds,
) async {
try {
return await _apiService.albumApi.createAlbum(
CreateAlbumDto(
albumName: albumName,
assetIds: assets.map((asset) => asset.id).toList(),
sharedWithUserIds: sharedUserIds,
),
);
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
return null;
}
}
Future<AlbumResponseDto?> getAlbumDetail(String albumId) async {
try {
return await _apiService.albumApi.getAlbumInfo(albumId);
} catch (e) {
debugPrint('Error [getAlbumDetail] ${e.toString()}');
return null;
}
}
Future<bool> addAdditionalAssetToAlbum(
Set<AssetResponseDto> assets,
String albumId,
) async {
try {
var result = await _apiService.albumApi.addAssetsToAlbum(
albumId,
AddAssetsDto(assetIds: assets.map((asset) => asset.id).toList()),
);
return result != null;
} catch (e) {
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
return false;
}
}
Future<bool> addAdditionalUserToAlbum(
List<String> sharedUserIds,
String albumId,
) async {
try {
var result = await _apiService.albumApi.addUsersToAlbum(
albumId,
AddUsersDto(sharedUserIds: sharedUserIds),
);
return result != null;
} catch (e) {
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
return false;
}
}
Future<bool> deleteAlbum(String albumId) async {
try {
await _apiService.albumApi.deleteAlbum(albumId);
return true;
} catch (e) {
debugPrint("Error deleteAlbum ${e.toString()}");
return false;
}
}
Future<bool> leaveAlbum(String albumId) async {
try {
await _apiService.albumApi.removeUserFromAlbum(albumId, "me");
return true;
} catch (e) {
debugPrint("Error deleteAlbum ${e.toString()}");
return false;
}
}
Future<bool> removeAssetFromAlbum(
String albumId,
List<String> assetIds,
) async {
try {
await _apiService.albumApi.removeAssetFromAlbum(
albumId,
RemoveAssetsDto(assetIds: assetIds),
);
return true;
} catch (e) {
debugPrint("Error deleteAlbum ${e.toString()}");
return false;
}
}
Future<bool> changeTitleAlbum(
String albumId,
String ownerId,
String newAlbumTitle,
) async {
try {
await _apiService.albumApi.updateAlbumInfo(
albumId,
UpdateAlbumDto(
albumName: newAlbumTitle,
),
);
return true;
} catch (e) {
debugPrint("Error deleteAlbum ${e.toString()}");
return false;
}
}
}

View File

@@ -5,15 +5,17 @@ class AlbumActionOutlinedButton extends StatelessWidget {
final String labelText;
final IconData iconData;
const AlbumActionOutlinedButton(
{Key? key,
this.onPressed,
required this.labelText,
required this.iconData})
: super(key: key);
const AlbumActionOutlinedButton({
Key? key,
this.onPressed,
required this.labelText,
required this.iconData,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon(
@@ -22,16 +24,23 @@ class AlbumActionOutlinedButton extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
side: const BorderSide(
side: BorderSide(
width: 1,
color: Color.fromARGB(255, 215, 215, 215),
color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
),
),
icon: Icon(iconData, size: 15),
icon: Icon(
iconData,
size: 15,
color: Theme.of(context).primaryColor,
),
label: Text(
labelText,
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
onPressed: onPressed,
),

View File

@@ -0,0 +1,93 @@
import 'dart:math';
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/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class AlbumThumbnailCard extends StatelessWidget {
const AlbumThumbnailCard({
Key? key,
required this.album,
required this.cacheService,
}) : super(key: key);
final AlbumResponseDto album;
final CacheService cacheService;
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
final cardSize = MediaQuery.of(context).size.width / 2 - 18;
return GestureDetector(
onTap: () {
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
},
child: Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
cacheManager: cacheService.getCache(CacheType.albumThumbnail),
memCacheHeight: max(400, cardSize.toInt() * 3),
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: "${album.albumThumbnailAssetId}",
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SizedBox(
width: cardSize,
child: 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

@@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
class AlbumTitleTextField extends ConsumerWidget {
const AlbumTitleTextField({
@@ -18,6 +19,8 @@ class AlbumTitleTextField extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return TextField(
onChanged: (v) {
if (v.isEmpty) {
@@ -30,7 +33,10 @@ class AlbumTitleTextField extends ConsumerWidget {
},
focusNode: albumTitleTextFieldFocusNode,
style: TextStyle(
fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
fontSize: 28,
color: Colors.grey[700],
fontWeight: FontWeight.bold,
),
controller: albumTitleController,
onTap: () {
isAlbumTitleTextFieldFocus.value = true;
@@ -47,7 +53,10 @@ class AlbumTitleTextField extends ConsumerWidget {
albumTitleController.clear();
isAlbumTitleEmpty.value = true;
},
icon: const Icon(Icons.cancel_rounded),
icon: Icon(
Icons.cancel_rounded,
color: Theme.of(context).primaryColor,
),
splashRadius: 10,
)
: null,
@@ -59,9 +68,11 @@ class AlbumTitleTextField extends ConsumerWidget {
borderSide: const BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
hintText: 'Add a title',
hintText: 'share_add_title'.tr(),
focusColor: Colors.grey[300],
fillColor: Colors.grey[200],
fillColor: isDarkTheme
? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200],
filled: isAlbumTitleTextFieldFocus.value,
),
);

View File

@@ -1,26 +1,28 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:openapi/api.dart';
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
const AlbumViewerAppbar({
Key? key,
required AsyncValue<SharedAlbum> albumInfo,
required this.albumInfo,
required this.userId,
required this.albumId,
}) : _albumInfo = albumInfo,
super(key: key);
}) : super(key: key);
final AsyncValue<SharedAlbum> _albumInfo;
final AlbumResponseDto albumInfo;
final String userId;
final String albumId;
@@ -37,15 +39,22 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess =
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
await ref.watch(albumServiceProvider).deleteAlbum(albumId);
if (isSuccess) {
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
if (albumInfo.shared) {
ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
ref.watch(albumProvider.notifier).deleteAlbum(albumId);
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [LibraryRoute()]));
}
} else {
ImmichToast.show(
context: context,
msg: "Failed to delete album",
msg: "album_viewer_appbar_share_err_delete".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
@@ -67,7 +76,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
Navigator.pop(context);
ImmichToast.show(
context: context,
msg: "Failed to leave album",
msg: "album_viewer_appbar_share_err_leave".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
@@ -93,7 +102,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
Navigator.pop(context);
ImmichToast.show(
context: context,
msg: "There are problems in removing assets from album",
msg: "album_viewer_appbar_share_err_remove".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
@@ -104,35 +113,35 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
_buildBottomSheetActionButton() {
if (isMultiSelectionEnable) {
if (_albumInfo.asData?.value.ownerId == userId) {
if (albumInfo.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_sweep_rounded),
title: const Text(
'Remove from album',
'album_viewer_appbar_share_remove',
style: TextStyle(fontWeight: FontWeight.bold),
),
).tr(),
onTap: () => _onRemoveFromAlbumPressed(albumId),
);
} else {
return Container();
return const SizedBox();
}
} else {
if (_albumInfo.asData?.value.ownerId == userId) {
if (albumInfo.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(
'Delete album',
'album_viewer_appbar_share_delete',
style: TextStyle(fontWeight: FontWeight.bold),
),
).tr(),
onTap: () => _onDeleteAlbumPressed(albumId),
);
} else {
return ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text(
'Leave album',
'album_viewer_appbar_share_leave',
style: TextStyle(fontWeight: FontWeight.bold),
),
).tr(),
onTap: () => _onLeaveAlbumPressed(albumId),
);
}
@@ -141,7 +150,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
void _buildBottomSheet() {
showModalBottomSheet(
backgroundColor: immichBackgroundColor,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: false,
context: context,
builder: (context) {
@@ -176,7 +185,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
if (!isSuccess) {
ImmichToast.show(
context: context,
msg: "Failed to change album title",
msg: "album_viewer_appbar_share_err_title".tr(),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
@@ -198,8 +207,8 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
elevation: 0,
leading: _buildLeadingButton(),
title: isMultiSelectionEnable
? Text(selectedAssetsInAlbum.length.toString())
: Container(),
? Text('${selectedAssetsInAlbum.length}')
: null,
centerTitle: false,
actions: [
IconButton(

View File

@@ -1,20 +1,24 @@
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/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:openapi/api.dart';
class AlbumViewerEditableTitle extends HookConsumerWidget {
final SharedAlbum albumInfo;
final AlbumResponseDto albumInfo;
final FocusNode titleFocusNode;
const AlbumViewerEditableTitle(
{Key? key, required this.albumInfo, required this.titleFocusNode})
: super(key: key);
const AlbumViewerEditableTitle({
Key? key,
required this.albumInfo,
required this.titleFocusNode,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final titleTextEditController =
useTextEditingController(text: albumInfo.albumName);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
void onFocusModeChange() {
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@@ -23,12 +27,15 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
}
useEffect(() {
titleFocusNode.addListener(onFocusModeChange);
return () {
titleFocusNode.removeListener(onFocusModeChange);
};
}, []);
useEffect(
() {
titleFocusNode.addListener(onFocusModeChange);
return () {
titleFocusNode.removeListener(onFocusModeChange);
};
},
[],
);
return TextField(
onChanged: (value) {
@@ -59,7 +66,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
onPressed: () {
titleTextEditController.clear();
},
icon: const Icon(Icons.cancel_rounded),
icon: Icon(
Icons.cancel_rounded,
color: Theme.of(context).primaryColor,
),
splashRadius: 10,
)
: null,
@@ -72,9 +82,11 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
borderRadius: BorderRadius.circular(10),
),
focusColor: Colors.grey[300],
fillColor: Colors.grey[200],
fillColor: isDarkTheme
? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200],
filled: titleFocusNode.hasFocus,
hintText: 'Add a title',
hintText: 'share_add_title'.tr(),
),
);
}

View File

@@ -6,21 +6,28 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class AlbumViewerThumbnail extends HookConsumerWidget {
final ImmichAsset asset;
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
const AlbumViewerThumbnail({
Key? key,
required this.asset,
required this.assetList,
this.showStorageIndicator = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
var thumbnailRequestUrl = getThumbnailUrl(asset);
var deviceId = ref.watch(authenticationProvider).deviceId;
final selectedAssetsInAlbumViewer =
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
@@ -28,24 +35,12 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() {
if (asset.type == 'IMAGE') {
AutoRouter.of(context).push(
ImageViewerRoute(
imageUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
heroTag: asset.id,
thumbnailUrl: thumbnailRequestUrl,
asset: asset,
),
);
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset),
);
}
AutoRouter.of(context).push(
GalleryViewerRoute(
asset: asset,
assetList: assetList,
),
);
}
BoxBorder drawBorderColor() {
@@ -71,29 +66,25 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
}
_buildVideoLabel() {
if (asset.type == 'IMAGE') {
return Container();
} else {
return Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
return Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
],
),
);
}
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
);
}
_buildAssetStoreLocationIcon() {
@@ -112,23 +103,20 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
_buildAssetSelectionIcon() {
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
if (isMultiSelectionEnable) {
return Positioned(
left: 10,
top: 5,
child: isSelected
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.white,
),
);
} else {
return Container();
}
return Positioned(
left: 10,
top: 5,
child: isSelected
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.white,
),
);
}
_buildThumbnailImage() {
@@ -177,16 +165,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
return GestureDetector(
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
onLongPress: _enableMultiSelection,
child: Hero(
tag: asset.id,
child: Stack(
children: [
_buildThumbnailImage(),
_buildAssetStoreLocationIcon(),
_buildVideoLabel(),
_buildAssetSelectionIcon(),
],
),
child: Stack(
children: [
_buildThumbnailImage(),
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
],
),
);
}

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
import 'package:openapi/api.dart';
class AssetGridByMonth extends HookConsumerWidget {
final List<ImmichAsset> assetGroup;
final List<AssetResponseDto> assetGroup;
const AssetGridByMonth({Key? key, required this.assetGroup})
: super(key: key);
@override

View File

@@ -1,16 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:openapi/api.dart';
class MonthGroupTitle extends HookConsumerWidget {
final String month;
final List<ImmichAsset> assetGroup;
final List<AssetResponseDto> assetGroup;
const MonthGroupTitle(
{Key? key, required this.month, required this.assetGroup})
: super(key: key);
const MonthGroupTitle({
Key? key,
required this.month,
required this.assetGroup,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -75,7 +77,11 @@ class MonthGroupTitle extends HookConsumerWidget {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
top: 29.0,
bottom: 29.0,
left: 14.0,
right: 8.0,
),
child: Row(
children: [
GestureDetector(
@@ -90,13 +96,16 @@ class MonthGroupTitle extends HookConsumerWidget {
color: Colors.grey,
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
_getSimplifiedMonth(),
style: TextStyle(
fontSize: 24,
color: Theme.of(context).primaryColor,
GestureDetector(
onTap: _handleTitleIconClick,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
_getSimplifiedMonth(),
style: TextStyle(
fontSize: 24,
color: Theme.of(context).primaryColor,
),
),
),
),

View File

@@ -4,11 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:openapi/api.dart';
class SelectionThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset;
final AssetResponseDto asset;
const SelectionThumbnailImage({Key? key, required this.asset})
: super(key: key);
@@ -18,25 +18,29 @@ class SelectionThumbnailImage extends HookConsumerWidget {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
var selectedAsset =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
Widget _buildSelectionIcon(ImmichAsset asset) {
if (selectedAsset.contains(asset) && !isAlbumExist) {
Widget _buildSelectionIcon(AssetResponseDto asset) {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isSelected && !isAlbumExist) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
} else if (selectedAsset.contains(asset) && isAlbumExist) {
} else if (isSelected && isAlbumExist) {
return const Icon(
Icons.check_circle,
color: Color.fromARGB(255, 233, 233, 233),
);
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
} else if (isNewlySelected && isAlbumExist) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
@@ -50,17 +54,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
}
BoxBorder drawBorderColor() {
if (selectedAsset.contains(asset) && !isAlbumExist) {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isSelected && !isAlbumExist) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
);
} else if (selectedAsset.contains(asset) && isAlbumExist) {
} else if (isSelected && isAlbumExist) {
return Border.all(
color: const Color.fromARGB(255, 190, 190, 190),
width: 10,
);
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
} else if (isNewlySelected && isAlbumExist) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
@@ -71,10 +79,15 @@ class SelectionThumbnailImage extends HookConsumerWidget {
return GestureDetector(
onTap: () {
var isSelected =
selectedAsset.map((item) => item.id).contains(asset.id);
var isNewlySelected =
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
if (isAlbumExist) {
// Operation for existing album
if (!selectedAsset.contains(asset)) {
if (newAssetsForAlbum.contains(asset)) {
if (!isSelected) {
if (isNewlySelected) {
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedAdditionalAssets([asset]);
@@ -86,7 +99,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
}
} else {
// Operation for new album
if (selectedAsset.contains(asset)) {
if (isSelected) {
ref
.watch(assetSelectionProvider.notifier)
.removeSelectedNewAssets([asset]);
@@ -103,7 +116,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
cacheKey: "${asset.id}-${cacheKey.value}",
width: 150,
height: 150,
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {
@@ -131,27 +144,26 @@ class SelectionThumbnailImage extends HookConsumerWidget {
child: _buildSelectionIcon(asset),
),
),
asset.type == 'IMAGE'
? Container()
: Positioned(
bottom: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
if (asset.type != AssetTypeEnum.IMAGE)
Positioned(
bottom: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
)
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
),
],
),
);

View File

@@ -4,10 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class SharedAlbumThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset;
final AssetResponseDto asset;
const SharedAlbumThumbnailImage({Key? key, required this.asset})
: super(key: key);
@@ -17,8 +18,6 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
return GestureDetector(
onTap: () {
@@ -30,9 +29,9 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
cacheKey: "${asset.id}-${cacheKey.value}",
width: 500,
height: 500,
memCacheHeight: asset.type == 'IMAGE' ? 500 : 500,
memCacheHeight: 500,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
imageUrl: getThumbnailUrl(asset),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) =>

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -14,8 +15,7 @@ class SharingSliverAppBar extends StatelessWidget {
floating: false,
pinned: true,
snap: false,
leading: Container(),
// elevation: 0,
automaticallyImplyLeading: false,
title: Text(
'IMMICH',
style: TextStyle(
@@ -35,47 +35,44 @@ class SharingSliverAppBar extends StatelessWidget {
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 4.0),
child: TextButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20)),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
child: ElevatedButton.icon(
onPressed: () {
AutoRouter.of(context)
.push(const CreateSharedAlbumRoute());
.push(CreateAlbumRoute(isSharedAlbum: true));
},
icon: const Icon(
Icons.photo_album_outlined,
size: 20,
),
label: const Text(
"Create shared album",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
"sharing_silver_appbar_create_shared_album",
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
// color: Theme.of(context).primaryColor,
),
).tr(),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: TextButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20)),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
child: ElevatedButton.icon(
onPressed: null,
icon: const Icon(
Icons.swap_horizontal_circle_outlined,
size: 20,
),
label: const Text(
"Share with partner",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
"sharing_silver_appbar_share_partner",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
).tr(),
),
),
)

View File

@@ -0,0 +1,305 @@
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/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:openapi/api.dart';
class AlbumViewerPage extends HookConsumerWidget {
final String albumId;
const AlbumViewerPage({Key? key, required this.albumId}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
FocusNode titleFocusNode = useFocusNode();
ScrollController scrollController = useScrollController();
var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
final userId = ref.watch(authenticationProvider).userId;
/// Find out if the assets in album exist on the device
/// If they exist, add to selected asset state to show they are already selected.
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
if (albumInfo.assets.isNotEmpty == true) {
ref
.watch(assetSelectionProvider.notifier)
.addNewAssets(albumInfo.assets.toList());
}
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
AssetSelectionPageResult? returnPayload = await AutoRouter.of(context)
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
if (returnPayload != null) {
// Check if there is new assets add
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
ImmichLoadingOverlayController.appLoader.show();
var isSuccess =
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
returnPayload.selectedAdditionalAsset,
albumId,
);
if (isSuccess) {
ref.refresh(sharedAlbumDetailProvider(albumId));
}
ImmichLoadingOverlayController.appLoader.hide();
}
ref.watch(assetSelectionProvider.notifier).removeAll();
} else {
ref.watch(assetSelectionProvider.notifier).removeAll();
}
}
void _onAddUsersPressed(AlbumResponseDto albumInfo) async {
List<String>? sharedUserIds =
await AutoRouter.of(context).push<List<String>?>(
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo),
);
if (sharedUserIds != null) {
ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await ref
.watch(albumServiceProvider)
.addAdditionalUserToAlbum(sharedUserIds, albumId);
if (isSuccess) {
ref.refresh(sharedAlbumDetailProvider(albumId));
}
ImmichLoadingOverlayController.appLoader.hide();
}
}
Widget _buildTitle(AlbumResponseDto albumInfo) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
child: userId == albumInfo.ownerId
? AlbumViewerEditableTitle(
albumInfo: albumInfo,
titleFocusNode: titleFocusNode,
)
: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
albumInfo.albumName,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) {
String startDate = "";
DateTime parsedStartDate =
DateTime.parse(albumInfo.assets.first.createdAt);
DateTime parsedEndDate = DateTime.parse(
albumInfo.assets.last.createdAt,
); //Need default.
if (parsedStartDate.year == parsedEndDate.year) {
startDate = DateFormat('LLL d').format(parsedStartDate);
} else {
startDate = DateFormat('LLL d, y').format(parsedStartDate);
}
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
return Padding(
padding: EdgeInsets.only(
left: 16.0,
top: 8.0,
bottom: albumInfo.shared ? 0.0 : 8.0,
),
child: Text(
"$startDate-$endDate",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
);
}
Widget _buildHeader(AlbumResponseDto albumInfo) {
return SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(albumInfo),
if (albumInfo.assets.isNotEmpty == true)
_buildAlbumDateRange(albumInfo),
if (albumInfo.shared)
SizedBox(
height: 60,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: CircleAvatar(
backgroundColor: Colors.grey[300],
radius: 18,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(50.0),
child: Image.asset(
'assets/immich-logo-no-outline.png',
),
),
),
),
);
}),
itemCount: albumInfo.sharedUsers.length,
),
)
],
),
);
}
Widget _buildImageGrid(AlbumResponseDto albumInfo) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final bool showStorageIndicator =
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
if (albumInfo.assets.isNotEmpty) {
return SliverPadding(
padding: const EdgeInsets.only(top: 10.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return AlbumViewerThumbnail(
asset: albumInfo.assets[index],
assetList: albumInfo.assets,
showStorageIndicator: showStorageIndicator,
);
},
childCount: albumInfo.assetCount,
),
),
);
}
return const SliverToBoxAdapter();
}
Widget _buildControlButton(AlbumResponseDto albumInfo) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
child: SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
AlbumActionOutlinedButton(
iconData: Icons.add_photo_alternate_outlined,
onPressed: () => _onAddPhotosPressed(albumInfo),
labelText: "share_add_photos".tr(),
),
if (userId == albumInfo.ownerId)
AlbumActionOutlinedButton(
iconData: Icons.person_add_alt_rounded,
onPressed: () => _onAddUsersPressed(albumInfo),
labelText: "album_viewer_page_share_add_users".tr(),
),
],
),
),
);
}
Widget _buildBody(AlbumResponseDto albumInfo) {
return GestureDetector(
onTap: () {
titleFocusNode.unfocus();
},
child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
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)
],
),
),
);
}
return Scaffold(
appBar: albumInfo.when(
data: (AlbumResponseDto? data) {
if (data != null) {
return AlbumViewerAppbar(
albumInfo: data,
userId: userId,
albumId: albumId,
);
}
return null;
},
error: (e, _) => null,
loading: () => null,
),
body: albumInfo.when(
data: (albumInfo) => albumInfo != null
? _buildBody(albumInfo)
: const Center(
child: CircularProgressIndicator(),
),
error: (e, _) => Center(child: Text("Error loading album info $e")),
loading: () => const Center(
child: ImmichLoadingIndicator(),
),
),
);
}
}

View File

@@ -1,16 +1,18 @@
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/sharing/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/asset_grid_by_month.dart';
import 'package:immich_mobile/modules/sharing/ui/month_group_title.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/ui/asset_grid_by_month.dart';
import 'package:immich_mobile/modules/album/ui/month_group_title.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
class AssetSelectionPage extends HookConsumerWidget {
const AssetSelectionPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ScrollController scrollController = useScrollController();
@@ -41,7 +43,7 @@ class AssetSelectionPage extends HookConsumerWidget {
return Stack(
children: [
DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor,
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
heightScrollThumb: 48.0,
child: CustomScrollView(
@@ -65,32 +67,31 @@ class AssetSelectionPage extends HookConsumerWidget {
),
title: selectedAssets.isEmpty
? const Text(
'Add photos',
'share_add_photos',
style: TextStyle(fontSize: 18),
)
).tr()
: Text(
_buildAssetCountText(),
style: const TextStyle(fontSize: 18),
),
centerTitle: false,
actions: [
(!isAlbumExist && selectedAssets.isNotEmpty) ||
(isAlbumExist && newAssetsForAlbum.isNotEmpty)
? TextButton(
onPressed: () {
var payload = AssetSelectionPageResult(
isAlbumExist: isAlbumExist,
selectedAdditionalAsset: newAssetsForAlbum,
selectedNewAsset: selectedAssets,
);
AutoRouter.of(context).pop(payload);
},
child: const Text(
"Add",
style: TextStyle(fontWeight: FontWeight.bold),
),
)
: Container()
if ((!isAlbumExist && selectedAssets.isNotEmpty) ||
(isAlbumExist && newAssetsForAlbum.isNotEmpty))
TextButton(
onPressed: () {
var payload = AssetSelectionPageResult(
isAlbumExist: isAlbumExist,
selectedAdditionalAsset: newAssetsForAlbum,
selectedNewAsset: selectedAssets,
);
AutoRouter.of(context).pop(payload);
},
child: const Text(
"share_add",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
],
),
body: _buildBody(),

View File

@@ -0,0 +1,272 @@
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/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
import 'package:immich_mobile/routing/router.dart';
// ignore: must_be_immutable
class CreateAlbumPage extends HookConsumerWidget {
bool isSharedAlbum;
CreateAlbumPage({Key? key, required this.isSharedAlbum}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumTitleController =
useTextEditingController.fromValue(TextEditingValue.empty);
final albumTitleTextFieldFocusNode = useFocusNode();
final isAlbumTitleTextFieldFocus = useState(false);
final isAlbumTitleEmpty = useState(true);
final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
_showSelectUserPage() {
AutoRouter.of(context).push(const SelectUserForSharingRoute());
}
void _onBackgroundTapped() {
albumTitleTextFieldFocusNode.unfocus();
isAlbumTitleTextFieldFocus.value = false;
if (albumTitleController.text.isEmpty) {
albumTitleController.text = 'create_album_page_untitled'.tr();
ref
.watch(albumTitleProvider.notifier)
.setAlbumTitle('create_album_page_untitled'.tr());
}
}
_onSelectPhotosButtonPressed() async {
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
if (selectedAsset == null) {
ref.watch(assetSelectionProvider.notifier).removeAll();
}
}
_buildTitleInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10,
left: 10,
),
child: AlbumTitleTextField(
isAlbumTitleEmpty: isAlbumTitleEmpty,
albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode,
albumTitleController: albumTitleController,
isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus,
),
);
}
_buildTitle() {
if (selectedAssets.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 200, left: 18),
child: Text(
'create_shared_album_page_share_add_assets',
style: Theme.of(context).textTheme.headline2?.copyWith(
fontSize: 12,
fontWeight: FontWeight.normal,
),
).tr(),
),
);
}
return const SliverToBoxAdapter();
}
_buildSelectPhotosButton() {
if (selectedAssets.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
alignment: Alignment.centerLeft,
padding:
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
side: BorderSide(
color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
onPressed: _onSelectPhotosButtonPressed,
icon: Icon(
Icons.add_rounded,
color: Theme.of(context).primaryColor,
),
label: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'create_shared_album_page_share_select_photos',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
).tr(),
),
),
),
);
}
return const SliverToBoxAdapter();
}
_buildControlButton() {
return Padding(
padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16),
child: SizedBox(
height: 30,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
AlbumActionOutlinedButton(
iconData: Icons.add_photo_alternate_outlined,
onPressed: _onSelectPhotosButtonPressed,
labelText: "share_add_photos".tr(),
),
],
),
),
);
}
_buildSelectedImageGrid() {
if (selectedAssets.isNotEmpty) {
return SliverPadding(
padding: const EdgeInsets.only(top: 16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return GestureDetector(
onTap: _onBackgroundTapped,
child: SharedAlbumThumbnailImage(
asset: selectedAssets.toList()[index],
),
);
},
childCount: selectedAssets.length,
),
),
);
}
return const SliverToBoxAdapter();
}
_createNonSharedAlbum() async {
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
);
if (newAlbum != null) {
ref.watch(albumProvider.notifier).getAllAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
}
}
return Scaffold(
appBar: AppBar(
elevation: 0,
centerTitle: false,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton(
onPressed: () {
ref.watch(assetSelectionProvider.notifier).removeAll();
AutoRouter.of(context).pop();
},
icon: const Icon(Icons.close_rounded),
),
title: Text(
'share_create_album',
style: Theme.of(context).textTheme.headline2?.copyWith(
color: Theme.of(context).primaryColor,
),
).tr(),
actions: [
if (isSharedAlbum)
TextButton(
onPressed: albumTitleController.text.isNotEmpty
? _showSelectUserPage
: null,
child: Text(
'create_shared_album_page_share'.tr(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
if (!isSharedAlbum)
TextButton(
onPressed: albumTitleController.text.isNotEmpty &&
selectedAssets.isNotEmpty
? _createNonSharedAlbum
: null,
child: Text(
'create_shared_album_page_create'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
],
),
body: GestureDetector(
onTap: _onBackgroundTapped,
child: CustomScrollView(
slivers: [
SliverAppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 5,
automaticallyImplyLeading: false,
pinned: true,
floating: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(66.0),
child: Column(
children: [
_buildTitleInputField(),
if (selectedAssets.isNotEmpty) _buildControlButton(),
],
),
),
),
_buildTitle(),
_buildSelectPhotosButton(),
_buildSelectedImageGrid(),
],
),
),
);
}
}

View File

@@ -0,0 +1,118 @@
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/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
class LibraryPage extends HookConsumerWidget {
const LibraryPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider);
final cacheService = ref.watch(cacheServiceProvider);
useEffect(
() {
ref.read(albumProvider.notifier).getAllAlbums();
return null;
},
[],
);
Widget _buildAppBar() {
return const SliverAppBar(
centerTitle: true,
floating: true,
pinned: false,
snap: false,
automaticallyImplyLeading: false,
title: Text(
'IMMICH',
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
);
}
Widget _buildCreateAlbumButton() {
return GestureDetector(
onTap: () {
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: MediaQuery.of(context).size.width / 2 - 18,
height: MediaQuery.of(context).size.width / 2 - 18,
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
Icons.add_rounded,
size: 28,
color: Theme.of(context).primaryColor,
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: const Text(
'library_page_new_album',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
)
],
),
);
}
return Scaffold(
body: CustomScrollView(
slivers: [
_buildAppBar(),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: const Text(
'library_page_albums',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
),
SliverPadding(
padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 50),
sliver: SliverToBoxAdapter(
child: Wrap(
spacing: 12,
children: [
_buildCreateAlbumButton(),
for (var album in albums)
AlbumThumbnailCard(
cacheService: cacheService,
album: album,
),
],
),
),
)
],
),
);
}
}

View File

@@ -1,30 +1,30 @@
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/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/shared/models/user.model.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
final SharedAlbum albumInfo;
final AlbumResponseDto albumInfo;
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<List<User>> suggestedShareUsers =
AsyncValue<List<UserResponseDto>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
final sharedUsersList = useState<Set<User>>({});
final sharedUsersList = useState<Set<UserResponseDto>>({});
_addNewUsersHandler() {
AutoRouter.of(context)
.pop(sharedUsersList.value.map((e) => e.id).toList());
}
_buildTileIcon(User user) {
_buildTileIcon(UserResponseDto user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
@@ -42,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
}
}
_buildUserList(List<User> users) {
_buildUserList(List<UserResponseDto> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {
@@ -54,9 +54,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
label: Text(
user.email,
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.bold),
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
),
),
@@ -68,14 +69,15 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
Wrap(
children: [...usersChip],
),
const Padding(
padding: EdgeInsets.all(16.0),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Suggestions',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold),
'select_additional_user_for_sharing_page_suggestions'.tr(),
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold,
),
),
),
ListView.builder(
@@ -86,13 +88,16 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
title: Text(
users[index].email,
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
onTap: () {
if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value
.where((selectedUser) =>
selectedUser.id != users[index].id)
.where(
(selectedUser) => selectedUser.id != users[index].id,
)
.toSet();
} else {
sharedUsersList.value = {
@@ -112,9 +117,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
title: const Text(
'Invite to album',
'share_invite',
style: TextStyle(color: Colors.black),
),
).tr(),
elevation: 0,
centerTitle: false,
leading: IconButton(
@@ -128,9 +133,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
onPressed:
sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
child: const Text(
"Add",
"share_add",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
).tr(),
)
],
),
@@ -138,7 +143,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
data: (users) {
for (var sharedUsers in albumInfo.sharedUsers) {
users.removeWhere(
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId);
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId,
);
}
return _buildUserList(users);

View File

@@ -1,33 +1,34 @@
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/sharing/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/user.model.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';
class SelectUserForSharingPage extends HookConsumerWidget {
const SelectUserForSharingPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersList = useState<Set<User>>({});
AsyncValue<List<User>> suggestedShareUsers =
final sharedUsersList = useState<Set<UserResponseDto>>({});
AsyncValue<List<UserResponseDto>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider);
_createSharedAlbum() async {
var isSuccess =
await ref.watch(sharedAlbumServiceProvider).createSharedAlbum(
var newAlbum =
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
);
if (isSuccess) {
if (newAlbum != null) {
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
@@ -36,11 +37,14 @@ class SelectUserForSharingPage extends HookConsumerWidget {
.navigate(const TabControllerRoute(children: [SharingRoute()]));
}
const ScaffoldMessenger(
child: SnackBar(content: Text('Failed to create album')));
ScaffoldMessenger(
child: SnackBar(
content: const Text('select_user_for_sharing_page_err_album').tr(),
),
);
}
_buildTileIcon(User user) {
_buildTileIcon(UserResponseDto user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
@@ -58,7 +62,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
}
}
_buildUserList(List<User> users) {
_buildUserList(List<UserResponseDto> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {
@@ -70,9 +74,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
label: Text(
user.email,
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.bold),
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
),
),
@@ -84,15 +89,16 @@ class SelectUserForSharingPage extends HookConsumerWidget {
Wrap(
children: [...usersChip],
),
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Suggestions',
Padding(
padding: const EdgeInsets.all(16.0),
child: const Text(
'select_user_for_sharing_page_share_suggestions',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold),
),
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold,
),
).tr(),
),
ListView.builder(
shrinkWrap: true,
@@ -102,13 +108,16 @@ class SelectUserForSharingPage extends HookConsumerWidget {
title: Text(
users[index].email,
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
onTap: () {
if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value
.where((selectedUser) =>
selectedUser.id != users[index].id)
.where(
(selectedUser) => selectedUser.id != users[index].id,
)
.toSet();
} else {
sharedUsersList.value = {
@@ -127,10 +136,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
title: const Text(
'Invite to album',
style: TextStyle(color: Colors.black),
),
title: Text(
'share_invite',
style: TextStyle(color: Theme.of(context).primaryColor),
).tr(),
elevation: 0,
centerTitle: false,
leading: IconButton(
@@ -141,12 +150,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
),
actions: [
TextButton(
onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
child: const Text(
"Create Album",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
))
style: TextButton.styleFrom(
primary: Theme.of(context).primaryColor,
),
onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
child: const Text(
"share_create_album",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
// color: Theme.of(context).primaryColor,
),
).tr(),
)
],
),
body: suggestedShareUsers.when(

View File

@@ -1,14 +1,17 @@
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: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/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:transparent_image/transparent_image.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class SharingPage extends HookConsumerWidget {
const SharingPage({Key? key}) : super(key: key);
@@ -17,51 +20,50 @@ class SharingPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
final List<SharedAlbum> sharedAlbums = ref.watch(sharedAlbumProvider);
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
final CacheService cacheService = ref.watch(cacheServiceProvider);
useEffect(() {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return null;
}, []);
useEffect(
() {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return null;
},
[],
);
_buildAlbumList() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId !=
null
? "$thumbnailRequestUrl/${sharedAlbums[index].albumThumbnailAssetId}"
: "https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60";
final album = sharedAlbums[index];
return ListTile(
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FadeInImage(
child: CachedNetworkImage(
width: 60,
height: 60,
memCacheHeight: 200,
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: NetworkImage(
thumbnailUrl,
headers: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
),
cacheManager:
cacheService.getCache(CacheType.sharedAlbumThumbnail),
imageUrl: getAlbumThumbnailUrl(album),
cacheKey: album.albumThumbnailAssetId,
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200),
),
),
title: Text(
sharedAlbums[index].albumName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
onTap: () {
AutoRouter.of(context)
@@ -83,7 +85,7 @@ class SharingPage extends HookConsumerWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // if you need this
side: const BorderSide(
color: Colors.black12,
color: Colors.grey,
width: 1,
),
),
@@ -93,31 +95,27 @@ class SharingPage extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 5.0, bottom: 5),
const Padding(
padding: EdgeInsets.only(left: 5.0, bottom: 5),
child: Icon(
Icons.offline_share_outlined,
size: 50,
color: Theme.of(context).primaryColor.withAlpha(200),
// color: Theme.of(context).primaryColor,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'EMPTY LIST',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
'sharing_page_empty_list',
style: Theme.of(context).textTheme.headline3,
).tr(),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Create shared albums to share photos and videos with people in your network.',
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
),
'sharing_page_description',
style: Theme.of(context).textTheme.bodyMedium,
).tr(),
),
],
),
@@ -131,15 +129,15 @@ class SharingPage extends HookConsumerWidget {
body: CustomScrollView(
slivers: [
const SharingSliverAppBar(),
const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
sliver: SliverToBoxAdapter(
child: Text(
"Shared albums",
child: const Text(
"sharing_page_album",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
).tr(),
),
),
sharedAlbums.isNotEmpty

View File

@@ -1,19 +1,26 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.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/services/image_viewer.service.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:openapi/api.dart';
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
final ImageViewerService _imageViewerService = ImageViewerService();
final ImageViewerService _imageViewerService;
final ShareService _shareService;
ImageViewerStateNotifier()
: super(ImageViewerPageState(
downloadAssetStatus: DownloadAssetStatus.idle));
ImageViewerStateNotifier(this._imageViewerService, this._shareService)
: super(
ImageViewerPageState(
downloadAssetStatus: DownloadAssetStatus.idle,
),
);
void downloadAsset(ImmichAsset asset, BuildContext context) async {
void downloadAsset(AssetResponseDto asset, BuildContext context) async {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
@@ -39,8 +46,23 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
}
void shareAsset(AssetResponseDto asset, BuildContext context) async {
showDialog(
context: context,
builder: (BuildContext buildContext) {
_shareService
.shareAsset(asset)
.then((_) => Navigator.of(buildContext).pop());
return const ShareDialog();
},
barrierDismissible: false,
);
}
}
final imageViewerStateProvider =
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
((ref) => ImageViewerStateNotifier()));
((ref) => ImageViewerStateNotifier(
ref.watch(imageViewerServiceProvider), ref.watch(shareServiceProvider))),
);

View File

@@ -1,33 +1,37 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:http/http.dart' as http;
import 'package:photo_manager/photo_manager.dart';
import 'package:path_provider/path_provider.dart';
final imageViewerServiceProvider =
Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
class ImageViewerService {
Future<bool> downloadAssetToDevice(ImmichAsset asset) async {
final ApiService _apiService;
ImageViewerService(this._apiService);
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
try {
String fileName = p.basename(asset.originalPath);
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
Uri filePath = Uri.parse(
"$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
var res = await http.get(
filePath,
headers: {
"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"
},
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.deviceAssetId,
asset.deviceId,
isThumb: false,
isWeb: false,
);
final AssetEntity? entity;
if (asset.type == 'IMAGE') {
if (asset.type == AssetTypeEnum.IMAGE) {
entity = await PhotoManager.editor.saveImage(
res.bodyBytes,
title: p.basename(asset.originalPath),
@@ -39,14 +43,10 @@ class ImageViewerService {
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
}
if (entity != null) {
return true;
}
return entity != null;
} catch (e) {
debugPrint("Error saving file $e");
return false;
}
return false;
}
}

View File

@@ -1,13 +1,13 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:intl/intl.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:latlong2/latlong.dart';
class ExifBottomSheet extends ConsumerWidget {
final ImmichAssetWithExif assetDetail;
final AssetResponseDto assetDetail;
const ExifBottomSheet({Key? key, required this.assetDetail})
: super(key: key);
@@ -15,171 +15,171 @@ class ExifBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
_buildMap() {
return (assetDetail.exifInfo!.latitude != null &&
assetDetail.exifInfo!.longitude != null)
? Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
height: 150,
width: MediaQuery.of(context).size.width,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(15)),
),
child: FlutterMap(
options: MapOptions(
center: LatLng(assetDetail.exifInfo!.latitude!,
assetDetail.exifInfo!.longitude!),
zoom: 16.0,
),
layers: [
TileLayerOptions(
urlTemplate:
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c'],
attributionBuilder: (_) {
return const Text(
"© OpenStreetMap",
style: TextStyle(fontSize: 10),
);
},
),
MarkerLayerOptions(
markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
point: LatLng(assetDetail.exifInfo!.latitude!,
assetDetail.exifInfo!.longitude!),
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png')),
),
],
),
],
),
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
height: 150,
width: MediaQuery.of(context).size.width,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(15)),
),
child: FlutterMap(
options: MapOptions(
center: LatLng(
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
),
)
: Container();
zoom: 16.0,
),
layers: [
TileLayerOptions(
urlTemplate:
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c'],
attributionBuilder: (_) {
return const Text(
"© OpenStreetMap",
style: TextStyle(fontSize: 10),
);
},
),
MarkerLayerOptions(
markers: [
Marker(
anchorPos: AnchorPos.align(AnchorAlign.top),
point: LatLng(
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
),
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png'),
),
),
],
),
],
),
),
);
}
_buildLocationText() {
return (assetDetail.exifInfo!.city != null &&
assetDetail.exifInfo!.state != null)
? Text(
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
style: TextStyle(
fontSize: 12,
color: Colors.grey[200],
fontWeight: FontWeight.bold),
)
: Container();
return Text(
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
style: TextStyle(
fontSize: 12,
color: Colors.grey[200],
fontWeight: FontWeight.bold,
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
child: ListView(
children: [
assetDetail.exifInfo?.dateTimeOriginal != null
? Text(
DateFormat('E, LLL d, y • h:mm a').format(
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
),
style: TextStyle(
color: Colors.grey[400],
fontWeight: FontWeight.bold,
fontSize: 14,
),
)
: Container(),
if (assetDetail.exifInfo?.dateTimeOriginal != null)
Text(
DateFormat('date_format'.tr()).format(
assetDetail.exifInfo!.dateTimeOriginal!,
),
style: TextStyle(
color: Colors.grey[400],
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
"Add Description...",
"exif_bottom_sheet_description",
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
),
),
).tr(),
),
// Location
assetDetail.exifInfo?.latitude != null
? Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
Text(
"LOCATION",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
_buildMap(),
_buildLocationText(),
Text(
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
)
],
if (assetDetail.exifInfo?.latitude != null)
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
)
: Container(),
Text(
"exif_bottom_sheet_location",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
).tr(),
if (assetDetail.exifInfo?.latitude != null &&
assetDetail.exifInfo?.longitude != null)
_buildMap(),
if (assetDetail.exifInfo?.city != null &&
assetDetail.exifInfo?.state != null)
_buildLocationText(),
Text(
"${assetDetail.exifInfo?.latitude?.toStringAsFixed(4)}, ${assetDetail.exifInfo?.longitude?.toStringAsFixed(4)}",
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
)
],
),
),
// Detail
assetDetail.exifInfo != null
? Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"DETAILS",
style:
TextStyle(fontSize: 11, color: Colors.grey[400]),
),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.image),
title: Text(
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: assetDetail.exifInfo?.exifImageHeight != null
? Text(
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
: Container(),
),
assetDetail.exifInfo?.make != null
? ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.camera),
title: Text(
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
style: const TextStyle(
fontWeight: FontWeight.bold),
),
subtitle: Text(
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
)
: Container()
],
if (assetDetail.exifInfo != null)
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
)
: Container()
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"exif_bottom_sheet_details",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
).tr(),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.image),
title: Text(
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: assetDetail.exifInfo?.exifImageHeight != null
? Text(
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ",
)
: null,
),
if (assetDetail.exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.camera),
title: Text(
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} ",
),
),
],
),
),
],
),
);

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