Compare commits

...

43 Commits

Author SHA1 Message Date
Immich Release Bot
7a25d359b7 Version v1.47.3 2023-02-16 03:38:44 +00:00
Michel Heusschen
7cfb257c00 feat(nginx): refactor + ipv6 (#1763)
* feat(nginx): refactor + ipv6 + increased buffer

* Revert changes to proxy buffering

* remove commented lines
2023-02-15 15:21:52 -06:00
Alex
b660240059 fix(web/server) uploaded asset in shared link not loaded (#1766)
* fix(web/server): Uploaded asset to shared link does not get added to the shared link/album

* remove unused code

* Add endpoints for each remove and add assets to shared link

* Update api

* Added deletion logic

* Convert callback to async/await

* Fix linter

* Fix test

* Fix server test

* added test

* Test coverage

* modify DTO

* Add notification

* fix test
2023-02-15 15:21:22 -06:00
Alex
125ec1e85f fix(web): using serverApi on the client request lead to uncaught error (#1767) 2023-02-15 13:09:28 -06:00
Michel Heusschen
d31b35873f feat(web): improve login screen (#1754) 2023-02-15 11:56:54 -06:00
Michel Heusschen
e1c520b9e7 feat(web): remove duplicate asset calls (#1764)
* feat(web): remove duplicate asset calls

* use source element instead of video.src
2023-02-15 11:56:19 -06:00
Michel Heusschen
1361f18964 feat(web): redirect to login from getting started (#1755) 2023-02-15 06:42:41 -06:00
Michel Heusschen
0f00f22212 fix(web): remove link header causing 502 errors (#1765) 2023-02-15 06:40:52 -06:00
Skyler Mäntysaari
0d543bbb0a feat(server/web): Initial support for RAF and SRW RAW formats (#1414)
* feat(server/web): Initial support for RAF and SRW RAW formats.

* It should return the promise.

* Better comment

* feat(server/web): file-uploader needed changes.

* Remove un-used imports

* The failing test.. is no longer failing.

* Run prettier

* Original implementation with just a catch block added.

* feat(server): Some tests and specific handling for the two raw formats

* feat(web): Helper for raw image type.

* Handling of mimetype on server

* Handling of mimetypes on web with a map

* Bring back the acceptedfile filter

* Fix the asset-upload tests after changes

* acceptedFile is not usable due to type being empty from browser.

* Switch needs to use lowercase variants.

* Address Discord comments

* feat(mobile): Library page rework (album sorting, favorites) (#1501)

* Add album sorting

* Change AppBar to match photos page behaviour

* Add buttons

* First crude implementation of the favorites page

* Clean up

* Add favorite button

* i18n

* Add star indicator to thumbnail

* Add favorite logic to separate provider and fix favorite behavior in album

* Review feedback (Add isFavorite variable)

* dev: style buttons

* dev: styled drop down button

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>

* feat(mobile): Tap to enter immersive mode on gallery viewer (#1546)

* feat(mobile): Removed stay logged in checkbox and made it enabled by default (#1550)

* removed stay logged in checkbox and made it enabled by default

* adds padding to login button

* removed all isSaveLogin

* fix: logout would re-login with previous credential upon app restart

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>

* chore(server): remove token when logged out (#1560)

* chore(mobile): invoke logout() on mobile app

* feat: add mechanism to delete token from logging out endpoint

* fix: set state after login sequence success

* fix: not removing token when logging out from OAuth

* fix: prettier

* refactor: using accessTokenId to delete

* chore: pr comments

* fix: test

* fix: test threshold

* feat(deployment): support docker secrets (#1254)

* Support secrets

* Rewrite to support sh

* Remove JWT_SECRET

* fix(mobile): Added flutter native splash and splash screens (#1520)

* rebasing

* added launch background image to repository

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>

* refactor(mobile): introduce Album & User classes (#1561)

replace usages of AlbumResponseDto with Album
replace usages of UserResponseDto with User

* feat(mobile): Multiselect add to favorite from the timeline (#1558)

* multiselect add to favorites

* feat(server): add updatedAt to Asset, Album and User (#1566)

* feat: add updatedAt info to DTO and generate api

* chore: remove unsued file

* chore: Add update statement to add/remove asset/user to album

* fix: test

* chore(server): update package-lock.json to match package.json (#1573)

* chore(server) Add user FK to album entity (#1569)

* chore(deps): bump docker/setup-buildx-action from 2.4.0 to 2.4.1 (#1575)

Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.4.0...v2.4.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-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>

* chore(server): make owner as required response for AlbumResponseDto (#1579)

* feat(GitHub): update bug and feature request template (#1584)

* dev: Reusing template from Home Assistant

* dev: add bug report template

* fix: template

* dev: change type

* dev:

* dev: add default labels

* dev: Add default title

* dev: add feature request template

* remove feature request from markdown

* dev: frontmatter

* fix(GitHub): feature request template

* fix(GitHub): feature request form has wrong type for textarea

* feat(mobile): Responsive layout improvements with a navigation rail and album grid (#1583)

* feat(proxy): Initial IPv6 support (#1577)

* fix(server): Create album response doesn't have owner property as required (#1704)

* feat(web): allow uploading more file types (#1570)

* feat(web): allow uploading more file types

* fix(web): make filename extension lowercase

* refactor(mobile): add Isar DB & Store class (#1574)

* refactor(mobile): add Isar DB & Store class

new Store: globally accessible key-value store like Hive (but based on Isar)

replace first few places of Hive usage with the new Store

* reduce max. DB size to prevent errors on older iOS devices

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>

* feat(mobile): Home screen customization options (#1563)

* Try staggered layout for home page

* Introduce setting for dynamic layout

* Fix some provider related bugs

* Make asset grouping configurable

* Add translation keys, refactor group title

* Rename enum values

* Fix enum names

* Reformat long if statement

* Fix timezone related bug

* Minor clean up

* Fix unit test

* Add second assets check back to home screen

* [Localizely] Translations update (#1707)

* fix(server): get shared link album info doesn't contain owner property (#1708)

* Version v1.46.0

* feat(server/web): file-uploader needed changes.

* Add raf and srw to the file names.

* Remember to add the extensions to fileSelector.

* Removed the getMimeType function on server as shouldn't be needed anymore.

* Revert "Removed the getMimeType function on server as shouldn't be needed anymore."

It is required still.

This reverts commit fc766dd0be.

* Should use proper mimetypes.

* fix linter

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Matthias Rupp <matthias.rupp@posteo.de>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: martyfuhry <martyfuhry@gmail.com>
Co-authored-by: James <jdm12989@gmail.com>
Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Immich Release Bot <bot@immich.app>
2023-02-14 13:54:28 -06:00
Alex Tran
86b3bdb90b chore(mobile): bump pubspec version 2023-02-13 21:21:44 -06:00
Alex
d47cdfb647 chore(post-release): add release note 2023-02-13 17:56:03 -06:00
Immich Release Bot
ac5c17e8be Version v1.47.2 2023-02-13 22:28:29 +00:00
Alex
db67093391 Revert "feat(web): avoid duplicate call + small refactor (#1731)" (#1750)
This reverts commit 53fb3a36f7.
2023-02-13 16:27:15 -06:00
Immich Release Bot
318fba6c97 Version v1.47.1 2023-02-13 21:57:33 +00:00
Alex
2d63fa80b4 fix(web)*: Lodash issue in Svelte (#1749) 2023-02-13 15:56:43 -06:00
Immich Release Bot
1dc211a046 Version v1.47.0 2023-02-13 20:04:27 +00:00
Alex
f71f379529 [Localizely] Translations update (#1747) 2023-02-13 14:01:23 -06:00
Alex
bee95b4977 chore: Update localizely setting file 2023-02-13 13:59:12 -06:00
Alex
9f8aaa57b6 chore(setup): Revert IPv6 setup in NGINX (#1744) 2023-02-13 13:31:20 -06:00
Alex
2c1aab154a feat(web): remove upload file limit with rxjs and improve import size (#1743)
* feat(web): remove upload file limit with rxjs

* refactor: remove exif

* refactor: remove unused code

* fix: import lodash-es instead of lodash

* refactor: optimize import
2023-02-13 13:18:11 -06:00
Alex
37cfac27b8 fix(mobile): Remove unsplash placeholder image and style empty places, objects (#1742) 2023-02-13 06:29:45 -06:00
Alex
11b2e2a6e2 chore(mobile): additional MD3 styling and refactor some code (#1741) 2023-02-13 05:05:31 +00:00
Alex
d555ee737b feat(mobile): spinning flower (#1740) 2023-02-12 22:49:53 -06:00
martyfuhry
12a6a7d95a feat(mobile): Uses profile photo for user avatar drawer (#1738)
* uses profile photo for user avatar drawer

* Added some styling to the profile picture

* made the whole profile photo a gesture detector

* fixed image updating

* invalidates cachednetworkimage when new profile photo is uploaded

* Revert "invalidates cachednetworkimage when new profile photo is uploaded"

This reverts commit 17c83be556.

* Add fadeInImage to loading user profile

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-02-13 03:32:16 +00:00
Michel Heusschen
caac3bfc95 fix(server): only update album when required (#1739)
* fix(server): only update album when required

* remove thumbnail from empty album
2023-02-12 19:26:24 +00:00
Michel Heusschen
05630776a0 fix(server): more asset upload validation and docs (#1720)
* fix(server): more asset upload validation and docs

* remove unused DTO

* changed Object.keys() to Object.values()

* apply patch to openapi generator for web

* revert CreateAssetDto assetType enum

* resolve merge conflict

* Revert "resolve merge conflict"

This reverts commit 0e00805187.
2023-02-11 23:54:07 -06:00
Michael Kreuzer
72c947cbaf fix(server): fix resolution in thumbnail generation (#1737)
* fix landscape images having lower resolution

* do not enlarge images when generating thumbnail
2023-02-11 22:48:18 -06:00
Michel Heusschen
53fb3a36f7 feat(web): avoid duplicate call + small refactor (#1731) 2023-02-11 22:36:26 -06:00
Matthias Rupp
6b3892987a dev(mobile): Fix freeze bug on app start (#1732)
* Group by date objects instead of strings

* Change OpenAPI code generation to wrap json decoding in
Change OpenAPI code generation to wrap decodeJson in compute

* Remove orig file

* Fix linter error

* Change drag handle date format

* Order timeline explictly from new to old

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-02-11 21:37:48 -06:00
martyfuhry
390919c439 automatically read pubspec.yaml to set iOS version and build number (#1734)
Co-authored-by: Marty Fuhry <marty@fuhry.farm>
2023-02-11 14:52:43 -06:00
Alex
09ab06ae6c chore(mobile): Upgrade to Flutter 3.7 (#1416) 2023-02-11 14:23:32 -06:00
martyfuhry
ad9373312b feat(mobile): Responsive display of exif data in bottom sheet (#1725)
* two column view of exif

* fixes padding

* fixed divider when no map

* fixed map visibility in two column
2023-02-10 20:31:15 -06:00
Michel Heusschen
bd71e087d4 refactor(web): added types and some small changes (#1722) 2023-02-10 16:17:39 -06:00
martyfuhry
c90dcde7cc cleaned up action bar, changed horizontal more to info button (#1727) 2023-02-10 16:11:09 -06:00
Michel Heusschen
d91cc3616b feat(web): make assets cachable (#1724) 2023-02-10 16:01:35 -06:00
martyfuhry
74cd3d66c6 fixed cloud download button (#1726) 2023-02-10 12:01:58 -06:00
martyfuhry
e6f9d9a31a feat(mobile): Shows a toast after adding to favorites (#1714)
* shows toast on adding assets to favorites

* add to favorites first

* typo
2023-02-10 00:05:39 -06:00
martyfuhry
b71a86142b fixed back button navigation with drawer (#1711) 2023-02-10 00:04:41 -06:00
martyfuhry
6e4ba6184b fixes safe area issue with multiselect and adds overscroll on main timeline to select bottom (#1718) 2023-02-10 00:02:26 -06:00
Immich Release Bot
b37162099e Version v1.46.1 2023-02-10 04:24:05 +00:00
Zack Pollard
dab74662e9 fix(server): fk constraint violation when updating to 1.46 with deleted users and albums (#1716)
Fixes #1715
2023-02-10 04:11:04 +00:00
bo0tzz
3d103046bc fix(ci): Add missing checkout step to prepare-release workflow (#1709) 2023-02-09 13:02:30 -06:00
Alex Tran
3ca62d9c55 Add changelogs 2023-02-09 13:00:28 -06:00
168 changed files with 4084 additions and 2260 deletions

View File

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

View File

@@ -55,6 +55,11 @@ jobs:
needs: build_mobile
steps:
- name: Checkout
uses: actions/checkout@v3
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
- name: Download APK
uses: actions/download-artifact@v3
with:

View File

@@ -19,7 +19,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
flutter-version: '3.7.3'
- name: Install dependencies
run: dart pub get

View File

@@ -49,7 +49,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
flutter-version: '3.7.3'
- name: Run tests
working-directory: ./mobile
run: flutter test
@@ -78,7 +78,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
flutter-version: '3.7.3'
- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2.27.0
with:

View File

@@ -34,3 +34,11 @@ download:
locale_code: pt-BR
- file: mobile/assets/i18n/pl-PL.json
locale_code: pl-PL
- file: mobile/assets/i18n/sk-SK.json
locale_code: sk-SK
- file: mobile/assets/i18n/zh-CN.json
locale_code: zh-CN
- file: mobile/assets/i18n/ru-RU.json
locale_code: ru-RU
- file: mobile/assets/i18n/cs-CZ.json
locale_code: cs-CZ

View File

@@ -2,6 +2,11 @@
xmlns:tools="http://schemas.android.com/tools">
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

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

View File

@@ -0,0 +1,6 @@
* Library page rework (album sorting, favorites).
* Tap to enter immersive mode on the gallery viewer.
* Removed stay logged in the checkbox and made it enabled by default.
* Multiselect add to favorites from the timeline.
* Responsive layout improvements with a navigation rail and album grid.
* Home screen customization options.

View File

@@ -0,0 +1,11 @@
* Adds over scroll at end of timeline to select images on the bottom
* Fixes back navigation with tab controller
* Shows a toast after adding to favorites
* Fixed download button style
* Cleaned up action bar, changed horizontal icon more to info icon
* Responsive display of exif data in bottom sheet
* Upgrade to Flutter 3.7
* Fix freeze bug on app start
* Uses profile photo for user avatar drawer
* Spinning flower
* Remove unsplash placeholder image and style empty places

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000307">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000209">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="36.955598">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="79.840593">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="75.445063">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="21.361905">
</testcase>

View File

@@ -4,7 +4,7 @@
"album_thumbnail_card_item": "1 položka",
"album_thumbnail_card_items": "{} položky",
"album_thumbnail_card_shared": "Sdíleno",
"album_viewer_appbar_share_delete": "odstranit album",
"album_viewer_appbar_share_delete": "Odstranit album",
"album_viewer_appbar_share_err_delete": "Nepodařilo se odstranit album",
"album_viewer_appbar_share_err_leave": "Nepodařilo se ukončit album",
"album_viewer_appbar_share_err_remove": "Při odstraňování souborů z alba se vyskytly problémy.",
@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "Opustit album",
"album_viewer_appbar_share_remove": "Odstranit z alba",
"album_viewer_page_share_add_users": "Přidat uživatele",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Nastavení rozložení mřížky fotografií",
"asset_list_settings_title": "Fotografická mřížka",
"backup_album_selection_page_albums_device": "Alba v zařízení ({})",
@@ -42,9 +46,9 @@
"backup_controller_page_background_turn_off": "Zakázat službu na pozadí",
"backup_controller_page_background_turn_on": "Povolit službu na pozadí",
"backup_controller_page_background_wifi": "Jen na WiFi",
"backup_controller_page_backup": "Zálohování",
"backup_controller_page_backup": "Zálohováno",
"backup_controller_page_backup_selected": "Vybrané: ",
"backup_controller_page_backup_sub": "Zálohování fotografií a videí",
"backup_controller_page_backup_sub": "Zálohované fotografie a videa",
"backup_controller_page_cancel": "Zrušit",
"backup_controller_page_created": "Vytvořeno: {}",
"backup_controller_page_desc_backup": "Zapněte zálohování na popředí, aby se nové položky automaticky nahrávaly na server při otevření aplikace.",
@@ -54,7 +58,7 @@
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informace o zálohování",
"backup_controller_page_none_selected": "Žádné vybrané",
"backup_controller_page_remainder": "Zbytek",
"backup_controller_page_remainder": "Zůstává",
"backup_controller_page_remainder_sub": "Zbývající fotografie a alba, která se mají zálohovat z výběru",
"backup_controller_page_select": "Vybrat",
"backup_controller_page_server_storage": "Serverové úložiště",
@@ -67,9 +71,9 @@
"backup_controller_page_total_sub": "Všechny jedinečné fotografie a videa z vybraných alb",
"backup_controller_page_turn_off": "Zakázat zálohování na popředí",
"backup_controller_page_turn_on": "Povolit zálohování na popředí",
"backup_controller_page_uploading_file_info": "Nahrávání informací o souborech",
"backup_err_only_album": "Nelze odstranit pouze album",
"backup_info_card_assets": "položky",
"backup_controller_page_uploading_file_info": "Informace o nahrávaném souboru",
"backup_err_only_album": "Nelze odstranit jediné vybrané album",
"backup_info_card_assets": "položek",
"cache_settings_album_thumbnails": "Náhledy stránek knihovny (položek {})",
"cache_settings_clear_cache_button": "Vymazat vyrovnávací paměť",
"cache_settings_clear_cache_button_title": "Vymaže vyrovnávací paměť aplikace. To výrazně ovlivní výkon aplikace, dokud se vyrovnávací paměť neobnoví.",
@@ -94,9 +98,9 @@
"create_shared_album_page_share": "Sdílet",
"create_shared_album_page_share_add_assets": "PŘIDAT",
"create_shared_album_page_share_select_photos": "Vybrat fotografie",
"daily_title_text_date": "EEEE, d MMMM",
"daily_title_text_date_year": "EEEE, d MMMM y",
"date_format": "EEEE, d MMMM y • H:mm",
"daily_title_text_date": "EEEE, d. MMMM",
"daily_title_text_date_year": "EEEE, d. MMMM y",
"date_format": "EEEE, d. MMMM y • H:mm",
"delete_dialog_alert": "Tyto položky budou trvale odstraněny z Immich a z vašeho zařízení.",
"delete_dialog_cancel": "Zrušit",
"delete_dialog_ok": "Vymazat",
@@ -108,16 +112,24 @@
"experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií",
"experimental_settings_subtitle": "Používejte na vlastní riziko!",
"experimental_settings_title": "Experimentální",
"favorites_page_title": "Oblíbené",
"home_page_add_to_album_conflicts": "Přidány {added} položky do alba {album}. {failed} položky jsou již v albu.",
"home_page_add_to_album_success": "Přidány položky {added} do alba {album}.",
"home_page_building_timeline": "Vytváraní časové osy",
"home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných albech.",
"library_page_albums": "Alba",
"library_page_favorites": "Oblíbené",
"library_page_new_album": "Nové album",
"login_form_button_text": "Přihlášení",
"library_page_sharing": "Sdílení",
"library_page_sort_created": "Naposledy vytvořené",
"library_page_sort_title": "Podle názvu alba",
"login_form_button_text": "Přihlásit se",
"login_form_email_hint": "tvůjmail@email.com",
"login_form_endpoint_hint": "http://ip-tvého-serveru:port/api",
"login_form_endpoint_url": "URL adresa serveru",
"login_form_err_http": "Prosím, uveďte http:// nebo https://",
"login_form_err_invalid_email": "Neplatný e-mail",
"login_form_err_invalid_url": "Neplatná URL",
"login_form_err_leading_whitespace": "Úvodní mezera",
"login_form_err_trailing_whitespace": "Koncová mezera",
"login_form_failed_get_oauth_server_config": "Chyba přihlášení pomocí OAuth, zkontrolujte adresu URL serveru",
@@ -158,7 +170,7 @@
"setting_notifications_title": "Oznámení",
"setting_notifications_total_progress_subtitle": "Celkový průběh nahrávání (hotové/celkové položky)",
"setting_notifications_total_progress_title": "Zobrazit celkový průběh zálohování na pozadí",
"setting_pages_app_bar_settings": "nastavení",
"setting_pages_app_bar_settings": "Nastavení",
"settings_require_restart": "Pro použití tohoto nastavení restartujte Immich",
"share_add": "Přidat",
"share_add_photos": "Přidat fotografie",
@@ -168,15 +180,15 @@
"share_invite": "Pozvat do alba",
"sharing_page_album": "Shared albums",
"sharing_page_description": "Vytvářejte sdílená alba a sdílejte fotografie a videa s lidmi ve vaší síti.",
"sharing_page_empty_list": "Prázný dopis",
"sharing_page_empty_list": "Prázdný dopis",
"sharing_silver_appbar_create_shared_album": "Vytvořit sdílené album",
"sharing_silver_appbar_share_partner": "Sdílet s partnerem",
"tab_controller_nav_library": "Knihovna",
"tab_controller_nav_photos": "Fotografie",
"tab_controller_nav_search": "Vyhledávání",
"tab_controller_nav_sharing": "Sdílení",
"theme_setting_asset_list_storage_indicator_title": "Zobrazit indikátor úložiště na dlaždicích zdrojů",
"theme_setting_asset_list_tiles_per_row_title": "Počet aktiv na řádek ({})",
"theme_setting_asset_list_storage_indicator_title": "Zobrazit indikátor úložiště na dlaždicích položek",
"theme_setting_asset_list_tiles_per_row_title": "Počet položek na řádek ({})",
"theme_setting_dark_mode_switch": "Tmavé téma",
"theme_setting_image_viewer_quality_subtitle": "Přizpůsobení kvality prohlížeče detailů",
"theme_setting_image_viewer_quality_title": "Kvalita prohlížeče obrázků",

View File

@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "Forlad album",
"album_viewer_appbar_share_remove": "Fjern fra album",
"album_viewer_page_share_add_users": "Tilføj brugere",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Indstillinger for billedgitterlayout",
"asset_list_settings_title": "Billedgitter",
"backup_album_selection_page_albums_device": "Albummer på enhed ({})",
@@ -108,17 +112,17 @@
"experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter",
"experimental_settings_subtitle": "Brug på eget ansvar!",
"experimental_settings_title": "Eksperimentelle",
"favorites_page_title": "Favorites",
"favorites_page_title": "Favoritter",
"home_page_add_to_album_conflicts": "Tilføjede {added} billeder og videoer til album {album}. {failed} billeder og videoer er allerede i albummet.",
"home_page_add_to_album_success": "Tilføjede {added} billeder og videoer til album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_building_timeline": "Bygger tidslinjen",
"home_page_first_time_notice": "Hvis dette er din første gang i appen, bedes du vælge en backup af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
"library_page_albums": "Albummer",
"library_page_favorites": "Favorites",
"library_page_favorites": "Favoritter",
"library_page_new_album": "Nyt album",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"library_page_sharing": "Delte",
"library_page_sort_created": "Senest oprettet",
"library_page_sort_title": "Albumtitel",
"login_form_button_text": "Log ind",
"login_form_email_hint": "din-e-mail@e-mail.com",
"login_form_endpoint_hint": "http://din-server-ip:port/api",

View File

@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "Album verlassen",
"album_viewer_appbar_share_remove": "Entferne vom Album",
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",

View File

@@ -14,8 +14,8 @@
"album_viewer_page_share_add_users": "Add users",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Albums on device ({})",
@@ -203,4 +203,4 @@
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
}
}

View File

@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "Abandonar álbum ",
"album_viewer_appbar_share_remove": "Eliminar del álbum ",
"album_viewer_page_share_add_users": "Añadir usuarios",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",

View File

@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "Poistu albumista",
"album_viewer_appbar_share_remove": "Poista albumista",
"album_viewer_page_share_add_users": "Lisää käyttäjiä",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Kuvaruudukon asettelu",
"asset_list_settings_title": "Kuvaruudukko",
"backup_album_selection_page_albums_device": "Laitteen albumit ({})",

View File

@@ -12,6 +12,10 @@
"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",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos",
"asset_list_settings_title": "Grille de photos",
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",

View File

@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "Lascia album",
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
"album_viewer_page_share_add_users": "Aggiungi utenti",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto",
"asset_list_settings_title": "Griglia foto",
"backup_album_selection_page_albums_device": "Albums sul device ({})",

View File

@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "アルバムから退会",
"album_viewer_appbar_share_remove": "アルバムから除外",
"album_viewer_page_share_add_users": "ユーザーを追加",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "グリッドに関する設定",
"asset_list_settings_title": "グリッド",
"backup_album_selection_page_albums_device": "端末上のアルバム数は {} だよ",

View File

@@ -11,7 +11,11 @@
"album_viewer_appbar_share_err_title": "Fout bij wijzigen album titel",
"album_viewer_appbar_share_leave": "Verlaat album",
"album_viewer_appbar_share_remove": "Verwijder uit album",
"album_viewer_page_share_add_users": "Voeg gebruiker toe",
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Foto grid layout instellingen",
"asset_list_settings_title": "Foto Grid",
"backup_album_selection_page_albums_device": "Albums op apparaat ({})",
@@ -108,17 +112,17 @@
"experimental_settings_new_asset_list_title": "Experimenteel foto grid inschakelen",
"experimental_settings_subtitle": "Gebruik op eigen risico!",
"experimental_settings_title": "Experimenteel",
"favorites_page_title": "Favorites",
"favorites_page_title": "Favorieten",
"home_page_add_to_album_conflicts": "{added} items toegevoegd aan album {album}. {failed} items staan al in het album.",
"home_page_add_to_album_success": "{added} items toegevoegd aan album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_building_timeline": "Tijdlijn opbouwen",
"home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
"library_page_albums": "Albums",
"library_page_favorites": "Favorites",
"library_page_favorites": "Favorieten",
"library_page_new_album": "Nieuw album",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"library_page_sharing": "Gedeeld",
"library_page_sort_created": "Meest recent gemaakt",
"library_page_sort_title": "Albumtitel",
"login_form_button_text": "Inloggen",
"login_form_email_hint": "jouwemail@email.com",
"login_form_endpoint_hint": "http://jouw-server-ip:port/api",

View File

@@ -12,6 +12,10 @@
"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",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Ustawienia układu siatki zdjęć",
"asset_list_settings_title": "Siatka Zdjęć",
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})",

View File

@@ -1,194 +1,206 @@
{
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"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",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"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",
"album_info_card_backup_album_excluded": "ИСКЛЮЧЕН",
"album_info_card_backup_album_included": "ВКЛЮЧЕН",
"album_thumbnail_card_item": "1 объект",
"album_thumbnail_card_items": "{} объектов",
"album_thumbnail_card_shared": Общий",
"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": "Добавить пользователей",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Настройки макета сетки фотографий",
"asset_list_settings_title": "Сетка фотографий",
"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_background_service_backup_failed_message": "Не удалось выполнить резервное копирование. Повторная попытка…",
"backup_background_service_connection_failed_message": "Не удалось подключиться к серверу. Повторная попытка...",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_current_upload_notification": "Загружается {}",
"backup_background_service_default_notification": "Поиск новых объектов…",
"backup_background_service_error_title": "Ошибка резервного копирования",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "Backup Albums",
"backup_background_service_in_progress_notification": "Резервное копирование ваших объектов…",
"backup_background_service_upload_failure_notification": "Ошибка загрузки {}",
"backup_controller_page_albums": "Резервное копирование альбомов",
"backup_controller_page_background_battery_info_link": "Показать как",
"backup_controller_page_background_battery_info_message": "Для наилучшего фонового резервного копирования отключите любые настройки оптимизации батареи, ограничивающие фоновую активность для Immich.\n\nПоскольку это зависит от устройства, найдите необходимую информацию для производителя вашего устройства.",
"backup_controller_page_background_battery_info_ok": "ОК",
"backup_controller_page_background_battery_info_title": "Battery optimizations",
"backup_controller_page_background_battery_info_title": "\nОптимизация батареи",
"backup_controller_page_background_charging": "Только во время зарядки",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_turn_off": "Выключить фоновый сервис",
"backup_controller_page_background_turn_on": "Включить фоновый сервис",
"backup_controller_page_background_wifi": "Только на WiFi",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selected: ",
"backup_controller_page_backup_sub": "Backed up photos and videos",
"backup_controller_page_cancel": "Cancel",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
"backup_controller_page_excluded": "Excluded: ",
"backup_controller_page_failed": "Failed ({})",
"backup_controller_page_filename": "File name: {} [{}]",
"backup_controller_page_background_configure_error": "Не удалось настроить фоновую службу",
"backup_controller_page_background_delay": "Отложить резервное копирование новых объектов: {}",
"backup_controller_page_background_description": "Включите фоновую службу для автоматического резервного копирования любых новых объектов без необходимости открывать приложение",
"backup_controller_page_background_is_off": "Автоматическое резервное копирование в фоновом режиме отключено",
"backup_controller_page_background_is_on": "Автоматическое резервное копирование в фоновом режиме включено",
"backup_controller_page_background_turn_off": "Выключить фоновую службу",
"backup_controller_page_background_turn_on": "Включить фоновую службу",
"backup_controller_page_background_wifi": "Только через WiFi",
"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": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые активы на сервер при открытии приложения.",
"backup_controller_page_excluded": "Исключены:",
"backup_controller_page_failed": "Неудачных ({})",
"backup_controller_page_filename": "Имя файла: {} [{}]",
"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": "Automatic foreground backup is off",
"backup_controller_page_status_on": "Automatic foreground backup is on",
"backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "Albums to be backup",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
"backup_controller_page_turn_off": "Turn off foreground backup",
"backup_controller_page_turn_on": "Turn on foreground backup",
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "Cannot remove the only album",
"backup_info_card_assets": "assets",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "Clear cache",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
"cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "Full images",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Thumbnails",
"cache_settings_statistics_title": "Cache usage",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_title": "Caching Settings",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"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": "Автоматическое резервное копирование в активном режиме выключено",
"backup_controller_page_status_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": "Выключить резервное копирование в активном режиме",
"backup_controller_page_turn_on": "Включить резервное копирование в активном режиме",
"backup_controller_page_uploading_file_info": "Загрузка информации о файле",
"backup_err_only_album": "Невозможно удалить единственный альбом",
"backup_info_card_assets": "объекты",
"cache_settings_album_thumbnails": "Миниатюры страниц библиотеки ({} объектов)",
"cache_settings_clear_cache_button": "Очистить кэш",
"cache_settings_clear_cache_button_title": "Очищает кэш приложения. Это значительно повлияет на производительность приложения, до тех пор, пока кэш не будет перестроен заново.",
"cache_settings_image_cache_size": "Размер кэша изображений ({} объектов)",
"cache_settings_statistics_album": "Миниатюры библиотеки",
"cache_settings_statistics_assets": "{} объектов ({})",
"cache_settings_statistics_full": "Полные изображения",
"cache_settings_statistics_shared": "Миниатюры общих альбомов",
"cache_settings_statistics_thumbnail": "Миниатюры",
"cache_settings_statistics_title": "Размер кэша",
"cache_settings_subtitle": "Управление кэшированием мобильного приложения Immich",
"cache_settings_thumbnail_size": "Размер кэша эскизов ({} объектов)",
"cache_settings_title": "Настройки кэширования",
"control_bottom_app_bar_add_to_album": "Добавить в альбом",
"control_bottom_app_bar_album_info": "{} файлов",
"control_bottom_app_bar_album_info_shared": "{} файлов · Общий",
"control_bottom_app_bar_create_new_album": "\nСоздать новый альбом",
"control_bottom_app_bar_delete": "Удалить",
"control_bottom_app_bar_share": "Поделиться",
"create_album_page_untitled": "Untitled",
"create_album_page_untitled": "Без названия",
"create_shared_album_page_create": "Создать",
"create_shared_album_page_share": "Share",
"create_shared_album_page_share_add_assets": "ADD ASSETS",
"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, yh:mm a",
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
"create_shared_album_page_share": "Поделиться",
"create_shared_album_page_share_add_assets": "ДОБАВИТЬ ОБЪЕКТЫ",
"create_shared_album_page_share_select_photos": "Выберите фотографии",
"daily_title_text_date": "ДН, МММ дд",
"daily_title_text_date_year": "ДН, МММ дд, гггг",
"date_format": "ДН, LLL д, гч:мм a",
"delete_dialog_alert": "Эти объекты будут безвозвратно удалены из приложения, а также с вашего устройства",
"delete_dialog_cancel": "Отменить",
"delete_dialog_ok": "Удалить",
"delete_dialog_title": "Удалить навсегда",
"exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"exif_bottom_sheet_description": "Добавить описание...",
"exif_bottom_sheet_details": "ПОДРОБНОСТИ",
"exif_bottom_sheet_location": "МЕСТОПОЛОЖЕНИЕ",
"experimental_settings_new_asset_list_subtitle": "Работа ведётся",
"experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий",
"experimental_settings_subtitle": "Используйте на свой страх и риск!",
"experimental_settings_title": "Экспериментальное",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"experimental_settings_title": "Экспериментальные функции",
"favorites_page_title": "Избранное",
"home_page_add_to_album_conflicts": "Добавлено {added} объектов в альбом {album}. Объекты {failed} уже есть в альбоме.",
"home_page_add_to_album_success": "Добавлено {added} объектов в альбом {album}.",
"home_page_building_timeline": "Построение временной шкалы",
"home_page_first_time_notice": "Если вы используете приложение впервые, убедитесь, что вы выбрали резервный(е) альбом(ы), чтобы временная шкала могла заполнить фотографии и видео в альбоме(ах).",
"library_page_albums": "Альбомы",
"library_page_favorites": "Избранное",
"library_page_new_album": "Новый альбом",
"login_form_button_text": "Login",
"library_page_sharing": "Общие",
"library_page_sort_created": "По новизне",
"library_page_sort_title": "По названию альбома",
"login_form_button_text": "Войти",
"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_get_oauth_server_config": "Error logging using OAuth, check server URL",
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
"login_form_failed_login": "Error logging you in, check server URL, email and password",
"login_form_endpoint_url": "URL Адрес сервера",
"login_form_err_http": "Пожалуйста, укажите http:// или https://",
"login_form_err_invalid_email": "Неверный адрес Email",
"login_form_err_invalid_url": "Неверная ссылка",
"login_form_err_leading_whitespace": "Пробел до",
"login_form_err_trailing_whitespace": "Пробел после",
"login_form_failed_get_oauth_server_config": "Ошибка авторизации с использованием OAuth, проверьте URL-адрес сервера",
"login_form_failed_get_oauth_server_disable": "Функция OAuth недоступна на этом сервере.",
"login_form_failed_login": "Ошибка при входе в систему, проверьте URL-адрес сервера, адрес электронной почты и пароль",
"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_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"login_form_label_password": "Пароль",
"login_form_password_hint": "пароль",
"login_form_save_login": "Оставаться в системе",
"monthly_title_text_date_format": "ММММ г",
"profile_drawer_app_logs": "Журналы",
"profile_drawer_client_server_up_to_date": "Клиент и сервер обновлены",
"profile_drawer_settings": "Настройки",
"profile_drawer_sign_out": "Выйти",
"search_bar_hint": "Search your photos",
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available",
"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": "Suggestions",
"select_user_for_sharing_page_err_album": "Failed to create album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"search_result_page_new_search_hint": "Новый поиск",
"select_additional_user_for_sharing_page_suggestions": "Предложения",
"select_user_for_sharing_page_err_album": "\nНе удалось создать альбом",
"select_user_for_sharing_page_share_suggestions": "Предложения",
"setting_image_viewer_help": "Средство просмотра деталей сначала загружает маленькую миниатюру, затем загружает предварительный просмотр среднего размера (если включено) и, наконец, загружает оригинал (если включено).",
"setting_image_viewer_original_subtitle": "Включите загрузку оригинального изображения в полном разрешении (большое!). Отключите, чтобы уменьшить объем данных (как в сети, так и в кеше устройства).",
"setting_image_viewer_original_title": "Загрузить исходное изображение",
"setting_image_viewer_preview_subtitle": "Включите загрузку изображения среднего разрешения. Отключите, чтобы загрузить оригинал напрямую или использовать только миниатюру.",
"setting_image_viewer_preview_title": "Загрузить изображение для предварительного просмотра",
"setting_notifications_notify_failures_grace_period": "Уведомлять об ошибках фонового резервного копирования: {}",
"setting_notifications_notify_hours": "{} часов",
"setting_notifications_notify_immediately": "немедленно",
"setting_notifications_notify_minutes": "{} минут",
"setting_notifications_notify_never": "никогда",
"setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
"setting_notifications_single_progress_title": "Show background backup detail progress",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_notify_seconds": "{} секунд",
"setting_notifications_single_progress_subtitle": "Подробная информация о ходе загрузки для каждого объекта",
"setting_notifications_single_progress_title": "Показать ход выполнения фонового резервного копирования",
"setting_notifications_subtitle": "Настроить параметры уведомлений",
"setting_notifications_title": "Уведомления",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_notifications_total_progress_subtitle": "Общий прогресс загрузки (выполнено/всего объектов)",
"setting_notifications_total_progress_title": "Показать общий прогресс фонового резервного копирования",
"setting_pages_app_bar_settings": "Настройки",
"settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add",
"share_add_photos": "Add photos",
"share_add_title": "Add a title",
"share_create_album": "Create album",
"share_dialog_preparing": "Preparing...",
"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_library": "Library",
"tab_controller_nav_photos": "Photos",
"settings_require_restart": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу",
"share_add": "Добавить",
"share_add_photos": "Добавить фото",
"share_add_title": "Добавить название",
"share_create_album": "Создать альбом",
"share_dialog_preparing": "Подготовка...",
"share_invite": "\nПригласить в альбом",
"sharing_page_album": "Общие альбомы",
"sharing_page_description": "Создавайте общие альбомы, чтобы делиться фотографиями и видео с людьми в вашей сети.",
"sharing_page_empty_list": "ПУСТОЙ СПИСОК",
"sharing_silver_appbar_create_shared_album": "Создать общий альбом",
"sharing_silver_appbar_share_partner": "Поделиться с партнером",
"tab_controller_nav_library": "Библиотека",
"tab_controller_nav_photos": "Фото",
"tab_controller_nav_search": "Поиск",
"tab_controller_nav_sharing": "Sharing",
"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 ({})",
"tab_controller_nav_sharing": "Общие",
"theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов",
"theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({})",
"theme_setting_dark_mode_switch": "Тёмная тема",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_system_theme_switch": "Автоматически (следовать системным настройкам)",
"theme_setting_image_viewer_quality_subtitle": "Настройте качество просмотра подробного изображения",
"theme_setting_image_viewer_quality_title": "Качество просмотра изображений",
"theme_setting_system_theme_switch": "Автоматически (Как в системе)",
"theme_setting_theme_subtitle": "Выберите настройки темы приложения",
"theme_setting_theme_title": "Тема",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"version_announcement_overlay_ack": "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.",
"theme_setting_three_stage_loading_subtitle": "Трехэтапная загрузка может повысить производительность загрузки, но вызывает значительно более высокую нагрузку на сеть",
"theme_setting_three_stage_loading_title": "Включить трехэтапную загрузку",
"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 или любой другой механизм, который обрабатывает обновление вашего серверного приложения автоматически.",
"version_announcement_overlay_title": "Доступна новая версия сервера \uD83C\uDF89"
}

View File

@@ -4,7 +4,7 @@
"album_thumbnail_card_item": "1 položka",
"album_thumbnail_card_items": "{} položky",
"album_thumbnail_card_shared": "Zdieľané",
"album_viewer_appbar_share_delete": "odstrániť album",
"album_viewer_appbar_share_delete": "Odstrániť album",
"album_viewer_appbar_share_err_delete": "Nepodarilo sa odstrániť album",
"album_viewer_appbar_share_err_leave": "Nepodarilo sa ukončiť album",
"album_viewer_appbar_share_err_remove": "Pri odstraňovaní súborov z albumu sa vyskytli problémy.",
@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "Opustiť album",
"album_viewer_appbar_share_remove": "Odstrániť z albumu",
"album_viewer_page_share_add_users": "Pridať používateľov",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Nastavenia rozloženia mriežky fotografií",
"asset_list_settings_title": "Fotografická mriežka",
"backup_album_selection_page_albums_device": "Albumy v zariadení ({})",
@@ -42,9 +46,9 @@
"backup_controller_page_background_turn_off": "Zakázať službu na pozadí",
"backup_controller_page_background_turn_on": "Povoliť službu na pozadí",
"backup_controller_page_background_wifi": "Len na WiFi",
"backup_controller_page_backup": "Zálohovanie",
"backup_controller_page_backup": "Zálohované",
"backup_controller_page_backup_selected": "Vybrané: ",
"backup_controller_page_backup_sub": "Zálohovanie fotografií a videí",
"backup_controller_page_backup_sub": "Zálohované fotografie a videa",
"backup_controller_page_cancel": "Zrušiť",
"backup_controller_page_created": "Vytvorené: {}",
"backup_controller_page_desc_backup": "Zapnite zálohovanie na popredí, aby sa nové položky automaticky nahrávali na server pri otvorení aplikácie.",
@@ -54,7 +58,7 @@
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informácie o zálohovaní",
"backup_controller_page_none_selected": "Žiadne vybrané",
"backup_controller_page_remainder": "Zvyšok",
"backup_controller_page_remainder": "Zostáva",
"backup_controller_page_remainder_sub": "Zostávajúce fotografie a albumy, ktoré sa majú zálohovať z výberu",
"backup_controller_page_select": "Vybrať",
"backup_controller_page_server_storage": "Serverové úložisko",
@@ -67,9 +71,9 @@
"backup_controller_page_total_sub": "Všetky jedinečné fotografie a videá z vybraných albumov",
"backup_controller_page_turn_off": "Zakázať zálohovanie na popredí",
"backup_controller_page_turn_on": "Povoliť zálohovanie na popredí",
"backup_controller_page_uploading_file_info": "Nahrávanie informácií o súboroch",
"backup_err_only_album": "Nie je možné odstrániť iba album",
"backup_info_card_assets": "položky",
"backup_controller_page_uploading_file_info": "Informácia o nahrávanom súbore",
"backup_err_only_album": "Nie je možné odstrániť jediný vybraný album",
"backup_info_card_assets": "položiek",
"cache_settings_album_thumbnails": "Náhľady stránok knižnice (položiek {})",
"cache_settings_clear_cache_button": "Vymazať vyrovnávaciu pamäť",
"cache_settings_clear_cache_button_title": "Vymaže vyrovnávaciu pamäť aplikácie. To výrazne ovplyvní výkon aplikácie, kým sa vyrovnávacia pamäť neobnoví.",
@@ -94,9 +98,9 @@
"create_shared_album_page_share": "Zdieľať",
"create_shared_album_page_share_add_assets": "PRIDAŤ",
"create_shared_album_page_share_select_photos": "Vybrať fotografie",
"daily_title_text_date": "EEEE, d MMMM",
"daily_title_text_date_year": "EEEE, d MMMM y",
"date_format": "EEEE, d MMMM y • H:mm",
"daily_title_text_date": "EEEE, d. MMMM",
"daily_title_text_date_year": "EEEE, d. MMMM y",
"date_format": "EEEE, d. MMMM y • H:mm",
"delete_dialog_alert": "Tieto položky budú natrvalo odstránené z Immich a z vášho zariadenia.",
"delete_dialog_cancel": "Zrušiť",
"delete_dialog_ok": "Vymazať",
@@ -108,16 +112,24 @@
"experimental_settings_new_asset_list_title": "Povolenie experimentálnej mriežky fotografií",
"experimental_settings_subtitle": "Používajte na vlastné riziko!",
"experimental_settings_title": "Experimentálne",
"favorites_page_title": "Obľúbené",
"home_page_add_to_album_conflicts": "Pridané {added} položky do albumu {album}. {failed} položky sú už v albume.",
"home_page_add_to_album_success": "Pridané {added} položky do albumu {album}.",
"home_page_building_timeline": "Vytváranie časovej osi",
"home_page_first_time_notice": "Ak aplikáciu používate prvýkrát, nezabudnite si vybrať zálohované albumy, aby sa na časovej osi mohli nachádzať fotografie a videá z vybraných albumoch.",
"library_page_albums": "Albumy",
"library_page_favorites": "Obľúbené",
"library_page_new_album": "Nový album",
"login_form_button_text": "Prihlásenie",
"library_page_sharing": "Zdieľanie",
"library_page_sort_created": "Najnovšie vytvorené",
"library_page_sort_title": "Podľa názvu albumu",
"login_form_button_text": "Prihlásiť sa",
"login_form_email_hint": "tvojmail@email.com",
"login_form_endpoint_hint": "http://ip-tvojho-servera:port/api",
"login_form_endpoint_url": "URL adresa servera",
"login_form_err_http": "Prosím, uveďte http:// alebo https://",
"login_form_err_invalid_email": "Neplatný e-mail",
"login_form_err_invalid_url": "Neplatná URL",
"login_form_err_leading_whitespace": "Úvodná medzera",
"login_form_err_trailing_whitespace": "Koncové medzera",
"login_form_failed_get_oauth_server_config": "Chyba prihlásenia pomocou OAuth, skontrolujte adresu URL servera",
@@ -141,7 +153,7 @@
"select_additional_user_for_sharing_page_suggestions": "Návrhy",
"select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album",
"select_user_for_sharing_page_share_suggestions": "Návrhy",
"setting_image_viewer_help": "V prehliadači detailov sa najprv načíta malá miniatúra, potom sa načíta náhľad strednej veľkosti (ak je povoleny) a nakoniec sa načíta originál (ak je povolený).",
"setting_image_viewer_help": "V prehliadači detailov sa najprv načíta malá miniatúra, potom sa načíta náhľad strednej veľkosti (ak je povolený) a nakoniec sa načíta originál (ak je povolený).",
"setting_image_viewer_original_subtitle": "Umožňuje načítať pôvodný obrázok v plnom rozlíšení (veľký!). Zakázať pre zníženie používania dát (v sieti aj v medzipamäti zariadenia).",
"setting_image_viewer_original_title": "Načítať pôvodný obrázok",
"setting_image_viewer_preview_subtitle": "Umožňuje načítať obrázok so stredným rozlíšením. Zakážte, ak chcete priamo načítať originál alebo použiť iba miniatúru.",
@@ -158,7 +170,7 @@
"setting_notifications_title": "Oznámenia",
"setting_notifications_total_progress_subtitle": "Celkový priebeh nahrávania (hotové/celkové položky)",
"setting_notifications_total_progress_title": "Zobraziť celkový priebeh zálohovania na pozadí",
"setting_pages_app_bar_settings": "nastavenia",
"setting_pages_app_bar_settings": "Nastavenia",
"settings_require_restart": "Na použitie tohto nastavenia reštartujte Immich",
"share_add": "Pridať",
"share_add_photos": "Pridať fotografie",
@@ -168,15 +180,15 @@
"share_invite": "Pozvať do albumu",
"sharing_page_album": "Shared albums",
"sharing_page_description": "Vytvárajte zdieľané albumy a zdieľajte fotografie a videá s ľuďmi vo vašej sieti.",
"sharing_page_empty_list": "Prázny list",
"sharing_page_empty_list": "Prázdny list",
"sharing_silver_appbar_create_shared_album": "Vytvoriť zdieľaný album",
"sharing_silver_appbar_share_partner": "Zdieľať s partnerom",
"tab_controller_nav_library": "Knižnica",
"tab_controller_nav_photos": "Fotografie",
"tab_controller_nav_search": "Vyhľadávanie",
"tab_controller_nav_sharing": "Zdieľanie",
"theme_setting_asset_list_storage_indicator_title": "Zobraziť indikátor úložiska na dlaždiciach zdrojov",
"theme_setting_asset_list_tiles_per_row_title": "Počet aktív na riadok ({})",
"theme_setting_asset_list_storage_indicator_title": "Zobraziť indikátor úložiska na dlaždiciach položiek",
"theme_setting_asset_list_tiles_per_row_title": "Počet položiek na riadok ({})",
"theme_setting_dark_mode_switch": "Tmavá téma",
"theme_setting_image_viewer_quality_subtitle": "Prispôsobenie kvality prehliadača detailov",
"theme_setting_image_viewer_quality_title": "Kvalita prehliadača obrázkov",

View File

@@ -12,6 +12,10 @@
"album_viewer_appbar_share_leave": "退出相册",
"album_viewer_appbar_share_remove": "从相册中移除",
"album_viewer_page_share_add_users": "新增用户",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "照片预览设置",
"asset_list_settings_title": "照片预览",
"backup_album_selection_page_albums_device": "设备上的相册({})",
@@ -108,16 +112,24 @@
"experimental_settings_new_asset_list_title": "启用实验性的照片宫格",
"experimental_settings_subtitle": "使用风险自负!",
"experimental_settings_title": "实验功能",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "添加{added}张到相册{album}。{failed} 项已经处于该相册中。",
"home_page_add_to_album_success": "添加了{added}张到相册{album}。",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"library_page_albums": "相册",
"library_page_favorites": "Favorites",
"library_page_new_album": "新建相册",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"login_form_button_text": "登录",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
"login_form_endpoint_url": "服务器地址",
"login_form_err_http": "请检查http://或https://",
"login_form_err_invalid_email": "请输入正确的邮箱",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "前面空格",
"login_form_err_trailing_whitespace": "后面空格",
"login_form_failed_get_oauth_server_config": "使用 OAuth 时出错,请检查服务器 地址",

View File

@@ -7,7 +7,8 @@ void main() async {
await ImmichTestHelper.initialize();
group("Login input validation test", () {
immichWidgetTest("Test leading/trailing whitespace", (tester, helper) async {
immichWidgetTest("Test leading/trailing whitespace",
(tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
@@ -17,15 +18,21 @@ void main() async {
await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_leading_whitespace".tr()), findsOneWidget);
expect(
find.text("login_form_err_leading_whitespace".tr()),
findsOneWidget,
);
await helper.loginHelper.enterCredentials(
email: "demo@immich.app ",
email: "demo@immich.app ",
);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_trailing_whitespace".tr()), findsOneWidget);
expect(
find.text("login_form_err_trailing_whitespace".tr()),
findsOneWidget,
);
});
immichWidgetTest("Test invalid email", (tester, helper) async {
@@ -33,13 +40,12 @@ void main() async {
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentials(
email: "demo.immich.app",
email: "demo.immich.app",
);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_invalid_email".tr()), findsOneWidget);
});
});
}

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '11.0'
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -34,19 +34,8 @@ target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
# post_install do |installer|
# installer.pods_project.targets.each do |target|
# flutter_additional_ios_build_settings(target)
# end
# end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
config.build_settings['ENABLE_BITCODE'] = 'YES'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@@ -1,5 +1,7 @@
PODS:
- Flutter (1.0.0)
- flutter_native_splash (0.0.1):
- Flutter
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
@@ -15,18 +17,22 @@ PODS:
- Flutter
- integration_test (0.0.1):
- Flutter
- isar_flutter_libs (1.0.0):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_ios (0.0.1):
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- photo_manager (2.0.0):
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
- share_plus (0.0.1):
- Flutter
- shared_preferences_ios (0.0.1):
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
@@ -40,16 +46,18 @@ PODS:
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/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`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/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 +72,8 @@ SPEC REPOS:
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_web_auth:
@@ -74,16 +84,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
isar_flutter_libs:
:path: ".symlinks/plugins/isar_flutter_libs/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/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"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
@@ -95,24 +107,26 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07
PODFILE CHECKSUM: c798208781ca5116c4a3d5927d689946791f0189
COCOAPODS: 1.11.3

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -201,6 +201,7 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -237,6 +238,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -341,7 +343,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
NEW_SETTING = "";
SDKROOT = iphoneos;
@@ -360,7 +362,8 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 84;
CURRENT_PROJECT_VERSION = 86;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -425,7 +428,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
NEW_SETTING = "";
ONLY_ACTIVE_ARCH = YES;
@@ -475,7 +478,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
NEW_SETTING = "";
SDKROOT = iphoneos;
@@ -495,7 +498,8 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 84;
CURRENT_PROJECT_VERSION = 86;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +526,8 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 84;
CURRENT_PROJECT_VERSION = 86;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

File diff suppressed because one or more lines are too long

View File

@@ -1,97 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Immich</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.47.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>86</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Immich</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.43.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>82</string>
<key>LSRequiresIPhoneOS</key>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSLocationAlwaysUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>en</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ru</string>
<string>sk</string>
<string>zh</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>
<key>NSLocationAlwaysUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>en</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ru</string>
<string>sk</string>
<string>zh</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>FLTEnableImpeller</key>
<true/>
</dict>
</plist>

View File

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

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.33276">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="3.705535">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.370222">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.23144">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.977555">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.423549">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="94.172587">
<testcase classname="fastlane.lanes" name="4: build_app" time="98.940158">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="82.579096">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="64.950609">
</testcase>

View File

@@ -91,7 +91,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
children: [
Text(
'Add to album',
style: Theme.of(context).textTheme.headline2,
style: Theme.of(context).textTheme.displayMedium,
),
TextButton.icon(
icon: const Icon(Icons.add),

View File

@@ -24,90 +24,97 @@ class AlbumThumbnailCard extends StatelessWidget {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return LayoutBuilder(
builder: (context, constraints) {
var cardSize = constraints.maxWidth;
var cardSize = constraints.maxWidth;
buildEmptyThumbnail() {
return Container(
height: cardSize,
width: cardSize,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
child: Center(
child: Icon(
Icons.no_photography,
size: cardSize * .15,
buildEmptyThumbnail() {
return Container(
height: cardSize,
width: cardSize,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
),
);
}
child: Center(
child: Icon(
Icons.no_photography,
size: cardSize * .15,
),
),
);
}
buildAlbumThumbnail() {
return CachedNetworkImage(
width: cardSize,
height: cardSize,
fit: BoxFit.cover,
fadeInDuration: const Duration(milliseconds: 200),
imageUrl: getAlbumThumbnailUrl(
album,
type: ThumbnailFormat.JPEG,
),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
);
}
buildAlbumThumbnail() {
return CachedNetworkImage(
width: cardSize,
height: cardSize,
fit: BoxFit.cover,
fadeInDuration: const Duration(milliseconds: 200),
imageUrl: getAlbumThumbnailUrl(
album,
type: ThumbnailFormat.JPEG,
),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
cacheKey:
getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
);
}
return GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return GestureDetector(
onTap: onTap,
child: Flex(
direction: Axis.vertical,
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: album.albumThumbnailAssetId == null
? buildEmptyThumbnail()
: buildAlbumThumbnail(),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SizedBox(
width: cardSize,
child: Text(
album.name,
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,
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: cardSize,
height: cardSize,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: album.albumThumbnailAssetId == null
? buildEmptyThumbnail()
: buildAlbumThumbnail(),
),
).tr()
],
)
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SizedBox(
width: cardSize,
child: Text(
album.name,
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

@@ -34,7 +34,7 @@ class AlbumTitleTextField extends ConsumerWidget {
focusNode: albumTitleTextFieldFocusNode,
style: TextStyle(
fontSize: 28,
color: Colors.grey[700],
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.bold,
),
controller: albumTitleController,

View File

@@ -19,7 +19,7 @@ class CreateAlbumPage extends HookConsumerWidget {
final List<Asset>? initialAssets;
const CreateAlbumPage({
Key? key,
Key? key,
required this.isSharedAlbum,
this.initialAssets,
}) : super(key: key);
@@ -84,7 +84,7 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 200, left: 18),
child: Text(
'create_shared_album_page_share_add_assets',
style: Theme.of(context).textTheme.headline2?.copyWith(
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 12,
fontWeight: FontWeight.normal,
),
@@ -214,7 +214,7 @@ class CreateAlbumPage extends HookConsumerWidget {
),
title: Text(
'share_create_album',
style: Theme.of(context).textTheme.headline2?.copyWith(
style: Theme.of(context).textTheme.displayMedium?.copyWith(
color: Theme.of(context).primaryColor,
),
).tr(),
@@ -228,7 +228,9 @@ class CreateAlbumPage extends HookConsumerWidget {
'create_shared_album_page_share'.tr(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
color: albumTitleController.text.isEmpty
? Theme.of(context).disabledColor
: Theme.of(context).primaryColor,
),
),
),

View File

@@ -15,6 +15,7 @@ class LibraryPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider);
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect(
() {
@@ -122,9 +123,12 @@ class LibraryPage extends HookConsumerWidget {
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
color: isDarkMode
? const Color.fromARGB(255, 53, 53, 53)
: const Color.fromARGB(255, 203, 203, 203),
),
borderRadius: BorderRadius.circular(8),
color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Icon(
@@ -168,25 +172,22 @@ class LibraryPage extends HookConsumerWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12.0,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.all(12),
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
side: BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[600]!
: Colors.grey[300]!,
color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
),
alignment: Alignment.centerLeft,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
icon: Icon(icon, color: Theme.of(context).primaryColor),
icon: Icon(
icon,
color: Theme.of(context).primaryColor,
),
),
);
}
@@ -253,7 +254,7 @@ class LibraryPage extends HookConsumerWidget {
delegate: SliverChildBuilderDelegate(
childCount: sorted.length + 1,
(context, index) {
if (index == 0) {
if (index == 0) {
return buildCreateAlbumButton();
}

View File

@@ -77,13 +77,13 @@ class SharingPage extends HookConsumerWidget {
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // if you need this
borderRadius: BorderRadius.circular(20),
side: const BorderSide(
color: Colors.grey,
width: 1,
width: 0.5,
),
),
color: Colors.transparent,
// color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Column(
@@ -92,7 +92,7 @@ class SharingPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.only(left: 5.0, bottom: 5),
child: Icon(
Icons.offline_share_outlined,
Icons.insert_photo_rounded,
size: 50,
color: Theme.of(context).primaryColor,
),
@@ -101,7 +101,7 @@ class SharingPage extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0),
child: Text(
'sharing_page_empty_list',
style: Theme.of(context).textTheme.headline3,
style: Theme.of(context).textTheme.displaySmall,
).tr(),
),
Padding(

View File

@@ -14,53 +14,60 @@ class ExifBottomSheet extends HookConsumerWidget {
const ExifBottomSheet({Key? key, required this.assetDetail})
: super(key: key);
bool get showMap => assetDetail.latitude != null && assetDetail.longitude != null;
@override
Widget build(BuildContext context, WidgetRef ref) {
buildMap() {
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.latitude ?? 0,
assetDetail.longitude ?? 0,
child: LayoutBuilder(
builder: (context, constraints) {
return Container(
height: 150,
width: constraints.maxWidth,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(15)),
),
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.latitude ?? 0,
assetDetail.longitude ?? 0,
),
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png'),
),
child: FlutterMap(
options: MapOptions(
interactiveFlags: InteractiveFlag.none,
center: LatLng(
assetDetail.latitude ?? 0,
assetDetail.longitude ?? 0,
),
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.latitude ?? 0,
assetDetail.longitude ?? 0,
),
builder: (ctx) => const Image(
image: AssetImage('assets/location-pin.png'),
),
),
],
),
],
),
],
),
);
},
),
);
}
@@ -91,6 +98,107 @@ class ExifBottomSheet extends HookConsumerWidget {
return text.isEmpty ? null : Text(text);
}
buildDragHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
SizedBox(height: 12),
Align(
alignment: Alignment.center,
child: CustomDraggingHandle(),
),
SizedBox(height: 12),
],
);
}
buildLocation() {
// Guard no lat/lng
if (!showMap) {
return Container();
}
return Column(
children: [
// Location
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"exif_bottom_sheet_location",
style: TextStyle(fontSize: 11, color: textColor),
).tr(),
buildMap(),
if (exifInfo != null &&
exifInfo.city != null &&
exifInfo.state != null)
buildLocationText(),
Text(
"${assetDetail.latitude!.toStringAsFixed(4)}, ${assetDetail.longitude!.toStringAsFixed(4)}",
style: const TextStyle(fontSize: 12),
)
],
),
],
);
}
buildDate() {
return Text(
DateFormat('date_format'.tr()).format(
assetDetail.createdAt.toLocal(),
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
);
}
buildDetail() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"exif_bottom_sheet_details",
style: TextStyle(fontSize: 11, color: textColor),
).tr(),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: const Icon(Icons.image),
title: Text(
assetDetail.fileName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
subtitle: buildSizeText(assetDetail),
),
if (exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: const Icon(Icons.camera),
title: Text(
"${exifInfo!.make} ${exifInfo.model}",
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ",
),
),
],
);
}
return SingleChildScrollView(
child: Card(
shape: const RoundedRectangleBorder(
@@ -102,104 +210,69 @@ class ExifBottomSheet extends HookConsumerWidget {
margin: const EdgeInsets.all(0),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Align(
alignment: Alignment.center,
child: CustomDraggingHandle(),
),
const SizedBox(height: 12),
Text(
DateFormat('date_format'.tr()).format(
assetDetail.createdAt.toLocal(),
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
// Location
if (assetDetail.latitude != null && assetDetail.longitude != null)
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
// Two column
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Divider(
thickness: 1,
buildDragHeader(),
buildDate(),
const SizedBox(height: 32.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: showMap ? 5 : 0,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: buildLocation(),
),
),
Flexible(
flex: 5,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: buildDetail(),
),
),
],
),
Text(
"exif_bottom_sheet_location",
style: TextStyle(fontSize: 11, color: textColor),
).tr(),
buildMap(),
if (exifInfo != null &&
exifInfo.city != null &&
exifInfo.state != null)
buildLocationText(),
Text(
"${assetDetail.latitude!.toStringAsFixed(4)}, ${assetDetail.longitude!.toStringAsFixed(4)}",
style: const TextStyle(fontSize: 12),
)
const SizedBox(height: 50),
],
),
),
// Detail
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
);
}
// One column
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildDragHeader(),
buildDate(),
const SizedBox(height: 16.0),
if (showMap)
Divider(
thickness: 1,
color: Colors.grey[600],
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"exif_bottom_sheet_details",
style: TextStyle(fontSize: 11, color: textColor),
).tr(),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: const Icon(Icons.image),
title: Text(
assetDetail.fileName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
subtitle: buildSizeText(assetDetail),
),
if (exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: const Icon(Icons.camera),
title: Text(
"${exifInfo!.make} ${exifInfo.model}",
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ",
),
),
],
),
),
const SizedBox(
height: 50,
),
],
const SizedBox(height: 16.0),
buildLocation(),
const SizedBox(height: 16.0),
Divider(
thickness: 1,
color: Colors.grey[600],
),
const SizedBox(height: 16.0),
buildDetail(),
const SizedBox(height: 50),
],
);
},
),
),
),

View File

@@ -31,12 +31,10 @@ class TopControlAppBar extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
double iconSize = 18.0;
const double iconSize = 18.0;
Widget buildFavoriteButton() {
return IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onFavorite();
},
@@ -60,12 +58,13 @@ class TopControlAppBar extends HookConsumerWidget {
color: Colors.grey[200],
),
),
actionsIconTheme: const IconThemeData(
size: iconSize,
),
actions: [
if (asset.isRemote) buildFavoriteButton(),
if (asset.livePhotoVideoId != null)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onToggleMotionVideo();
},
@@ -81,17 +80,13 @@ class TopControlAppBar extends HookConsumerWidget {
),
if (!asset.isLocal)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: onDownloadPressed,
icon: Icon(
Icons.cloud_download_rounded,
Icons.cloud_download_outlined,
color: Colors.grey[200],
),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onSharePressed();
},
@@ -102,8 +97,6 @@ class TopControlAppBar extends HookConsumerWidget {
),
if (asset.isRemote)
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onAddToAlbumPressed();
},
@@ -113,8 +106,6 @@ class TopControlAppBar extends HookConsumerWidget {
),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onDeletePressed();
},
@@ -124,13 +115,11 @@ class TopControlAppBar extends HookConsumerWidget {
),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onMoreInfoPressed();
},
icon: Icon(
Icons.more_horiz_rounded,
Icons.info_outline_rounded,
color: Colors.grey[200],
),
),

View File

@@ -15,7 +15,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/shared/services/asset.service.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
@@ -213,7 +213,7 @@ class GalleryViewerPage extends HookConsumerWidget {
void handleSwipeUpDown(DragUpdateDetails details) {
int sensitivity = 15;
int dxThreshhold = 50;
int dxThreshold = 50;
if (isZoomed.value) {
return;
@@ -222,7 +222,7 @@ class GalleryViewerPage extends HookConsumerWidget {
// Check for delta from initial down point
final d = details.localPosition - localPosition;
// If the magnitude of the dx swipe is large, we probably didn't mean to go down
if (d.dx.abs() > dxThreshhold) {
if (d.dx.abs() > dxThreshold) {
return;
}
@@ -247,8 +247,8 @@ class GalleryViewerPage extends HookConsumerWidget {
isPlayingMotionVideo: isPlayingMotionVideo.value,
asset: assetList[indexOfAsset.value],
isFavorite: ref.watch(favoriteProvider).contains(
assetList[indexOfAsset.value].id,
),
assetList[indexOfAsset.value].id,
),
onMoreInfoPressed: () {
showInfo();
},
@@ -314,7 +314,7 @@ class GalleryViewerPage extends HookConsumerWidget {
? (context, event) {
final asset = assetList[indexOfAsset.value];
if (!asset.isLocal) {
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
// Three-Stage Loading (WEBP -> JPEG -> Original)
final webPThumbnail = CachedNetworkImage(
imageUrl: getThumbnailUrl(

View File

@@ -14,11 +14,15 @@ class BackupInfoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this
side: const BorderSide(
color: Colors.black12,
borderRadius: BorderRadius.circular(20), // if you need this
side: BorderSide(
color: isDarkMode
? const Color.fromARGB(255, 101, 101, 101)
: Colors.black12,
width: 1,
),
),

View File

@@ -81,10 +81,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: GestureDetector(
onTap: removeSelection,
child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
label: Text(
album.name,
style: TextStyle(
@@ -119,10 +115,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
label: Text(
album.name,
style: TextStyle(

View File

@@ -31,6 +31,7 @@ class BackupControllerPage extends HookConsumerWidget {
!hasExclusiveAccess
? false
: true;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect(
() {
@@ -421,9 +422,11 @@ class BackupControllerPage extends HookConsumerWidget {
buildFolderSelectionTile() {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this
side: const BorderSide(
color: Colors.black12,
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: isDarkMode
? const Color.fromARGB(255, 101, 101, 101)
: Colors.black12,
width: 1,
),
),
@@ -476,6 +479,69 @@ class BackupControllerPage extends HookConsumerWidget {
}
}
Widget buildBackupButton() {
return Padding(
padding: const EdgeInsets.only(
top: 24,
),
child: Container(
child: backupState.backupProgress == BackUpProgressEnum.inProgress
? ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.grey[50],
backgroundColor: Colors.red[300],
// padding: const EdgeInsets.all(14),
),
onPressed: () {
ref.read(backupProvider.notifier).cancelBackup();
},
child: const Text(
"backup_controller_page_cancel",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
).tr(),
)
: ElevatedButton(
onPressed: shouldBackup ? startBackup : null,
child: const Text(
"backup_controller_page_start_backup",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
).tr(),
),
),
);
}
buildBackgroundBackupInfo() {
return hasExclusiveAccess
? const SizedBox.shrink()
: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // if you need this
side: BorderSide(
color: isDarkMode
? const Color.fromARGB(255, 101, 101, 101)
: Colors.black12,
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
"Background backup is currently running, some actions are disabled",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
);
}
return Scaffold(
appBar: AppBar(
elevation: 0,
@@ -506,27 +572,7 @@ class BackupControllerPage extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
).tr(),
),
hasExclusiveAccess
? const SizedBox.shrink()
: Card(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(5), // if you need this
side: const BorderSide(
color: Colors.black12,
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
"Background backup is currently running, some actions are disabled",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
buildBackgroundBackupInfo(),
buildFolderSelectionTile(),
BackupInfoCard(
title: "backup_controller_page_total".tr(),
@@ -552,42 +598,7 @@ class BackupControllerPage extends HookConsumerWidget {
buildStorageInformation(),
const Divider(),
const CurrentUploadingAssetInfoBox(),
Padding(
padding: const EdgeInsets.only(
top: 24,
),
child: Container(
child:
backupState.backupProgress == BackUpProgressEnum.inProgress
? ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.grey[50],
backgroundColor: Colors.red[300],
// padding: const EdgeInsets.all(14),
),
onPressed: () {
ref.read(backupProvider.notifier).cancelBackup();
},
child: const Text(
"backup_controller_page_cancel",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
).tr(),
)
: ElevatedButton(
onPressed: shouldBackup ? startBackup : null,
child: const Text(
"backup_controller_page_start_backup",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
).tr(),
),
),
)
buildBackupButton()
],
),
),

View File

@@ -75,22 +75,23 @@ class RenderList {
RenderList(this.elements);
static Map<String, List<Asset>> _groupAssets(
static Map<DateTime, List<Asset>> _groupAssets(
List<Asset> assets,
GroupAssetsBy groupBy,
) {
assets.sortByCompare<DateTime>(
(e) => e.createdAt,
(a, b) => b.compareTo(a),
);
if (groupBy == GroupAssetsBy.day) {
return assets.groupListsBy(
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
(element) {
final date = element.createdAt.toLocal();
return DateTime(date.year, date.month, date.day);
},
);
} else if (groupBy == GroupAssetsBy.month) {
return assets.groupListsBy(
(element) => DateFormat('y-MM').format(element.createdAt.toLocal()),
(element) {
final date = element.createdAt.toLocal();
return DateTime(date.year, date.month);
},
);
}
@@ -113,10 +114,11 @@ class RenderList {
final groups = _groupAssets(allAssets, groupBy);
groups.forEach((groupName, assets) {
try {
final date = assets.first.createdAt.toLocal();
groups.entries.sortedBy((e) =>e.key).reversed.forEach((entry) {
final date = entry.key;
final assets = entry.value;
try {
// Month title
if (groupBy == GroupAssetsBy.day &&
(lastDate == null || lastDate!.month != date.month)) {

View File

@@ -92,11 +92,10 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
RenderAssetGridRow row,
bool scrolling,
) {
return LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth / widget.assetsPerRow -
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
return Row(
key: Key("asset-row-${row.assets.first.id}"),
children: row.assets.mapIndexed((int index, Asset asset) {
@@ -141,7 +140,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.headline1?.color,
color: Theme.of(context).textTheme.displayLarge?.color,
),
),
);
@@ -164,7 +163,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
Text _labelBuilder(int pos) {
final date = widget.renderList.elements[pos].date;
return Text(
DateFormat.yMMMd().format(date),
DateFormat.yMMMM().format(date),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
@@ -189,6 +188,9 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
}
final listWidget = ScrollablePositionedList.builder(
padding: const EdgeInsets.only(
bottom: 220,
),
itemBuilder: _itemBuilder,
itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController,

View File

@@ -22,7 +22,7 @@ class MonthlyTitleText extends StatelessWidget {
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.headline1?.color,
color: Theme.of(context).textTheme.displayLarge?.color,
),
),
),

View File

@@ -2,7 +2,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/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/models/album.dart';
@@ -29,6 +29,8 @@ class ControlBottomAppBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
Widget renderActionButtons() {
return Row(
children: [
@@ -60,7 +62,6 @@ class ControlBottomAppBar extends ConsumerWidget {
);
},
),
],
);
}
@@ -75,7 +76,9 @@ class ControlBottomAppBar extends ConsumerWidget {
ScrollController scrollController,
) {
return Card(
elevation: 12.0,
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
surfaceTintColor: Colors.transparent,
elevation: 18.0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
@@ -83,45 +86,37 @@ class ControlBottomAppBar extends ConsumerWidget {
),
),
margin: const EdgeInsets.all(0),
child: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverToBoxAdapter(
child: Column(
children: <Widget>[
const SizedBox(height: 12),
const CustomDraggingHandle(),
const SizedBox(height: 12),
renderActionButtons(),
const Divider(
indent: 16,
endIndent: 16,
thickness: 1,
),
AddToAlbumTitleRow(onCreateNewAlbum: onCreateNewAlbum),
],
),
),
),
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverToBoxAdapter(
child: Column(
children: <Widget>[
const SizedBox(height: 12),
const CustomDraggingHandle(),
const SizedBox(height: 12),
renderActionButtons(),
const Divider(
indent: 16,
endIndent: 16,
thickness: 1,
),
AddToAlbumTitleRow(onCreateNewAlbum: onCreateNewAlbum),
],
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: AddToAlbumSliverList(
albums: albums,
sharedAlbums: sharedAlbums,
onAddToAlbum: onAddToAlbum,
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: AddToAlbumSliverList(
albums: albums,
sharedAlbums: sharedAlbums,
onAddToAlbum: onAddToAlbum,
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 200),
)
],
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 200),
)
],
),
);
},

View File

@@ -1,7 +1,11 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:badges/badges.dart';
import 'package:flutter/material.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/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -9,15 +13,16 @@ import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/transparent_image.dart';
class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
const HomePageAppBar({
Key? key,
super.key,
this.onPopBack,
}) : super(key: key);
});
final Function? onPopBack;
@@ -27,9 +32,48 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
bool isEnableAutoBackup = backupState.backgroundBackup ||
ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
AuthenticationState authState = ref.watch(authenticationProvider);
buildProfilePhoto() {
if (authState.profileImagePath.isEmpty) {
return IconButton(
splashRadius: 25,
icon: const Icon(
Icons.face_outlined,
size: 30,
),
onPressed: () {
Scaffold.of(context).openDrawer();
},
);
} else {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
var dummy = Random().nextInt(1024);
return InkWell(
onTap: () {
Scaffold.of(context).openDrawer();
},
child: CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
radius: 18,
child: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: FadeInImage.memoryNetwork(
fit: BoxFit.cover,
placeholder: kTransparentImage,
width: 33,
height: 33,
image:
'$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
fadeInDuration: const Duration(milliseconds: 200),
),
),
),
);
}
}
return AppBar(
centerTitle: true,
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
@@ -40,19 +84,8 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
builder: (BuildContext context) {
return Stack(
children: [
Positioned(
top: 5,
child: IconButton(
splashRadius: 25,
icon: Icon(
Icons.face_outlined,
size: 30,
color: Theme.of(context).primaryColor,
),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
Center(
child: buildProfilePhoto(),
),
if (serverInfoState.isVersionMismatch)
Positioned(
@@ -112,16 +145,13 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
splashRadius: 25,
iconSize: 30,
icon: isEnableAutoBackup
? Icon(
? const Icon(
Icons.backup_rounded,
color: Theme.of(context).primaryColor,
)
: Badge(
padding: const EdgeInsets.all(4),
elevation: 3,
position: BadgePosition.bottomEnd(bottom: -4, end: -4),
badgeColor: Colors.white,
badgeContent: const Icon(
backgroundColor: Colors.white,
label: const Icon(
Icons.cloud_off_rounded,
size: 8,
color: Colors.indigo,

View File

@@ -15,7 +15,7 @@ class ProfileDrawer extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
buildSignoutButton() {
buildSignOutButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
@@ -95,6 +95,9 @@ class ProfileDrawer extends HookConsumerWidget {
}
return Drawer(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -105,7 +108,7 @@ class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawerHeader(),
buildSettingButton(),
buildAppLogButton(),
buildSignoutButton(),
buildSignOutButton(),
],
),
const ServerInfoBox()

View File

@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/home/providers/upload_profile_image.provid
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/transparent_image.dart';
class ProfileDrawerHeader extends HookConsumerWidget {
const ProfileDrawerHeader({
@@ -22,10 +23,27 @@ class ProfileDrawerHeader extends HookConsumerWidget {
AuthenticationState authState = ref.watch(authenticationProvider);
final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status;
var dummmy = Random().nextInt(1024);
var dummy = Random().nextInt(1024);
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
buildUserProfileImage() {
var userImage = CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
radius: 35,
child: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: FadeInImage.memoryNetwork(
fit: BoxFit.cover,
placeholder: kTransparentImage,
width: 66,
height: 66,
image:
'$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
fadeInDuration: const Duration(milliseconds: 200),
),
),
);
if (authState.profileImagePath.isEmpty) {
return const CircleAvatar(
radius: 35,
@@ -36,16 +54,10 @@ class ProfileDrawerHeader extends HookConsumerWidget {
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
if (authState.profileImagePath.isNotEmpty) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
return userImage;
} else {
return const CircleAvatar(
radius: 35,
radius: 33,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
@@ -53,13 +65,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
}
if (uploadProfileImageStatus == UploadProfileStatus.success) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
return userImage;
}
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
@@ -98,7 +104,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
useEffect(
() {
buildUserProfileImage();
// buildUserProfileImage();
return null;
},
[],
@@ -126,17 +132,17 @@ class ProfileDrawerHeader extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
buildUserProfileImage(),
Positioned(
bottom: 0,
right: -5,
child: GestureDetector(
onTap: pickUserProfileImage,
GestureDetector(
onTap: pickUserProfileImage,
child: Stack(
clipBehavior: Clip.none,
children: [
buildUserProfileImage(),
Positioned(
bottom: 0,
right: -5,
child: Material(
color: Colors.grey[100],
color: isDarkMode ? Colors.grey[700] : Colors.grey[100],
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0),
@@ -151,8 +157,8 @@ class ProfileDrawerHeader extends HookConsumerWidget {
),
),
),
),
],
],
),
),
Text(
"${authState.firstName} ${authState.lastName}",

View File

@@ -101,10 +101,20 @@ class HomePage extends HookConsumerWidget {
}
void onFavoriteAssets() {
final remoteAssests = remoteOnlySelection(
final remoteAssets = remoteOnlySelection(
localErrorMessage: 'Can not favorite local assets yet, skipping',
);
ref.watch(favoriteProvider.notifier).addToFavorites(remoteAssests);
if (remoteAssets.isNotEmpty) {
ref.watch(favoriteProvider.notifier).addToFavorites(remoteAssets);
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg: 'Added ${remoteAssets.length} $assetOrAssets to favorites',
gravity: ToastGravity.BOTTOM,
);
}
selectionEnabledHook.value = false;
}
@@ -216,7 +226,6 @@ class HomePage extends HookConsumerWidget {
}
return SafeArea(
bottom: !multiselectEnabled.state,
top: true,
child: Stack(
children: [
@@ -234,14 +243,17 @@ class HomePage extends HookConsumerWidget {
selectionActive: selectionEnabledHook.value,
),
if (selectionEnabledHook.value)
ControlBottomAppBar(
onShare: onShareAssets,
onFavorite: onFavoriteAssets,
onDelete: onDelete,
onAddToAlbum: onAddToAlbum,
albums: albums,
sharedAlbums: sharedAlbums,
onCreateNewAlbum: onCreateNewAlbum,
SafeArea(
bottom: true,
child: ControlBottomAppBar(
onShare: onShareAssets,
onFavorite: onFavoriteAssets,
onDelete: onDelete,
onAddToAlbum: onAddToAlbum,
albums: albums,
sharedAlbums: sharedAlbums,
onCreateNewAlbum: onCreateNewAlbum,
),
),
],
),
@@ -249,11 +261,9 @@ class HomePage extends HookConsumerWidget {
}
return Scaffold(
appBar: multiselectEnabled.state
? null
: HomePageAppBar(
onPopBack: reloadAllAsset,
),
appBar: HomePageAppBar(
onPopBack: reloadAllAsset,
),
drawer: const ProfileDrawer(),
body: buildBody(),
);

View File

@@ -32,6 +32,9 @@ class LoginForm extends HookConsumerWidget {
final isLoading = useState<bool>(false);
final isOauthEnable = useState<bool>(false);
final oAuthButtonLabel = useState<String>('OAuth');
final logoAnimationController = useAnimationController(
duration: const Duration(seconds: 60),
)..repeat();
getServeLoginConfig() async {
if (!serverEndpointFocusNode.hasFocus) {
@@ -100,10 +103,13 @@ class LoginForm extends HookConsumerWidget {
children: [
GestureDetector(
onDoubleTap: () => populateTestLoginInfo(),
child: const Image(
image: AssetImage('assets/immich-logo-no-outline.png'),
width: 100,
filterQuality: FilterQuality.high,
child: RotationTransition(
turns: logoAnimationController,
child: const Image(
image: AssetImage('assets/immich-logo-no-outline.png'),
width: 100,
filterQuality: FilterQuality.high,
),
),
),
Text(

View File

@@ -53,6 +53,9 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
},
decoration: InputDecoration(
hintText: 'search_bar_hint'.tr(),
hintStyle: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),

View File

@@ -7,18 +7,21 @@ class ThumbnailWithInfo extends StatelessWidget {
const ThumbnailWithInfo({
Key? key,
required this.textInfo,
required this.imageUrl,
this.imageUrl,
this.noImageIcon,
required this.onTap,
}) : super(key: key);
final String textInfo;
final String imageUrl;
final String? imageUrl;
final Function onTap;
final IconData? noImageIcon;
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var textAndIconColor = isDarkMode ? Colors.grey[100] : Colors.grey[700];
return GestureDetector(
onTap: () {
onTap();
@@ -31,26 +34,37 @@ class ThumbnailWithInfo extends StatelessWidget {
alignment: Alignment.bottomCenter,
children: [
Container(
foregroundDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black26,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: CachedNetworkImage(
width: 250,
height: 250,
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
border: Border.all(
color: isDarkMode ? Colors.grey[800]! : Colors.grey[400]!,
width: 1,
),
),
child: imageUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(20),
child: CachedNetworkImage(
width: 250,
height: 250,
fit: BoxFit.cover,
imageUrl: imageUrl!,
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
),
)
: Center(
child: Icon(
noImageIcon ?? Icons.not_listed_location,
color: textAndIconColor,
),
),
),
Positioned(
bottom: 8,
left: 10,
bottom: 12,
left: 14,
child: SizedBox(
width: MediaQuery.of(context).size.width / 3,
child: Text(
@@ -58,7 +72,7 @@ class ThumbnailWithInfo extends StatelessWidget {
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
fontSize: 12,
),
),
),

View File

@@ -85,9 +85,7 @@ class SearchPage extends HookConsumerWidget {
itemCount: 1,
itemBuilder: ((context, index) {
return ThumbnailWithInfo(
imageUrl:
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
textInfo: 'search_page_no_places'.tr(),
textInfo: '',
onTap: () {},
);
}),
@@ -140,9 +138,8 @@ class SearchPage extends HookConsumerWidget {
itemCount: 1,
itemBuilder: ((context, index) {
return ThumbnailWithInfo(
imageUrl:
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
textInfo: 'search_page_no_objects'.tr(),
textInfo: '',
noImageIcon: Icons.signal_cellular_no_sim_sharp,
onTap: () {},
);
}),

View File

@@ -0,0 +1,68 @@
import 'dart:typed_data';
final Uint8List kTransparentImage = Uint8List.fromList(<int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
]);

View File

@@ -11,7 +11,6 @@ class TabControllerPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
navigationRail(TabsRouter tabsRouter) {
return NavigationRail(
labelType: NavigationRailLabelType.all,
@@ -35,32 +34,33 @@ class TabControllerPage extends ConsumerWidget {
right: 4,
bottom: 4,
),
icon: const Icon(Icons.photo_outlined),
icon: const Icon(Icons.photo_outlined),
selectedIcon: const Icon(Icons.photo),
label: const Text('tab_controller_nav_photos').tr(),
),
NavigationRailDestination(
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.search_rounded),
selectedIcon: const Icon(Icons.search),
icon: const Icon(Icons.search_rounded),
selectedIcon: const Icon(Icons.search),
label: const Text('tab_controller_nav_search').tr(),
),
NavigationRailDestination(
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.share_rounded),
selectedIcon: const Icon(Icons.share),
icon: const Icon(Icons.share_rounded),
selectedIcon: const Icon(Icons.share),
label: const Text('tab_controller_nav_sharing').tr(),
),
NavigationRailDestination(
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.photo_album_outlined),
selectedIcon: const Icon(Icons.photo_album),
icon: const Icon(Icons.photo_album_outlined),
selectedIcon: const Icon(Icons.photo_album),
label: const Text('tab_controller_nav_library').tr(),
),
],
);
}
// ignore: unused_element
bottomNavigationBar(TabsRouter tabsRouter) {
return BottomNavigationBar(
selectedLabelStyle: const TextStyle(
@@ -101,6 +101,58 @@ class TabControllerPage extends ConsumerWidget {
);
}
experimentalNavigationBar(TabsRouter tabsRouter) {
return NavigationBar(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: (index) {
HapticFeedback.selectionClick();
tabsRouter.setActiveIndex(index);
},
destinations: [
NavigationDestination(
label: 'tab_controller_nav_photos'.tr(),
icon: const Icon(
Icons.photo_outlined,
),
selectedIcon: Icon(
Icons.photo,
color: Theme.of(context).primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_search'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: Theme.of(context).primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_sharing'.tr(),
icon: const Icon(
Icons.group_outlined,
),
selectedIcon: Icon(
Icons.group,
color: Theme.of(context).primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_library'.tr(),
icon: const Icon(
Icons.photo_album_outlined,
),
selectedIcon: Icon(
Icons.photo_album_rounded,
color: Theme.of(context).primaryColor,
),
)
],
);
}
final multiselectEnabled = ref.watch(multiselectProvider);
return AutoTabsRouter(
routes: [
@@ -111,15 +163,13 @@ class TabControllerPage extends ConsumerWidget {
],
builder: (context, child, animation) {
final tabsRouter = AutoTabsRouter.of(context);
final appRouter = AutoRouter.of(context);
return WillPopScope(
onWillPop: () async {
bool atHomeTab = tabsRouter.activeIndex == 0;
if (!atHomeTab) {
tabsRouter.setActiveIndex(0);
} else {
appRouter.navigateBack();
}
return atHomeTab;
},
child: LayoutBuilder(
@@ -129,7 +179,7 @@ class TabControllerPage extends ConsumerWidget {
final Widget body;
if (constraints.maxWidth < medium) {
// Normal phone width
bottom = bottomNavigationBar(tabsRouter);
bottom = experimentalNavigationBar(tabsRouter);
body = FadeTransition(
opacity: animation,
child: child,
@@ -148,13 +198,13 @@ class TabControllerPage extends ConsumerWidget {
),
],
);
} return Scaffold(
body: body,
bottomNavigationBar: multiselectEnabled
? null
: bottom,
);
},),
}
return Scaffold(
body: body,
bottomNavigationBar: multiselectEnabled ? null : bottom,
);
},
),
);
},
);

View File

@@ -20,6 +20,93 @@ final immichThemeProvider = StateProvider<ThemeMode>((ref) {
}
});
ThemeData base = ThemeData(
chipTheme: const ChipThemeData(
side: BorderSide.none,
),
);
ThemeData immichLightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
primaryColor: Colors.indigo,
hintColor: Colors.indigo,
fontFamily: 'WorkSans',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
appBarTheme: AppBarTheme(
titleTextStyle: const TextStyle(
fontFamily: 'WorkSans',
color: Colors.indigo,
),
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedItemColor: Colors.indigo,
),
cardTheme: const CardTheme(
surfaceTintColor: Colors.transparent,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichBackgroundColor,
),
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
displayMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
displaySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
),
chipTheme: base.chipTheme,
popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
surfaceTintColor: Colors.transparent,
color: Colors.white,
),
navigationBarTheme: NavigationBarThemeData(
indicatorColor: Colors.indigo.withOpacity(0.15),
iconTheme: MaterialStatePropertyAll(
IconThemeData(color: Colors.grey[700]),
),
backgroundColor: immichBackgroundColor,
surfaceTintColor: Colors.transparent,
labelTextStyle: MaterialStatePropertyAll(
TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
),
);
ThemeData immichDarkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
@@ -43,7 +130,8 @@ ThemeData immichDarkTheme = ThemeData(
),
backgroundColor: const Color.fromARGB(255, 32, 33, 35),
foregroundColor: immichDarkThemePrimaryColor,
elevation: 1,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
@@ -56,17 +144,17 @@ ThemeData immichDarkTheme = ThemeData(
scrimColor: Colors.white.withOpacity(0.1),
),
textTheme: TextTheme(
headline1: const TextStyle(
displayLarge: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255),
),
headline2: const TextStyle(
displayMedium: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255),
),
headline3: TextStyle(
displaySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: immichDarkThemePrimaryColor,
@@ -79,57 +167,26 @@ ThemeData immichDarkTheme = ThemeData(
backgroundColor: immichDarkThemePrimaryColor,
),
),
);
ThemeData immichLightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
hintColor: Colors.indigo,
fontFamily: 'WorkSans',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
appBarTheme: AppBarTheme(
titleTextStyle: const TextStyle(
fontFamily: 'WorkSans',
color: Colors.indigo,
chipTheme: base.chipTheme,
popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 1,
centerTitle: true,
surfaceTintColor: Colors.transparent,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedItemColor: Colors.indigo,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichBackgroundColor,
),
textTheme: const TextTheme(
headline1: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.indigo,
navigationBarTheme: NavigationBarThemeData(
indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4),
iconTheme: MaterialStatePropertyAll(
IconThemeData(color: Colors.grey[500]),
),
headline2: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
headline3: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
backgroundColor: Colors.grey[900],
surfaceTintColor: Colors.transparent,
labelTextStyle: MaterialStatePropertyAll(
TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[500],
),
),
),
);

View File

@@ -86,7 +86,6 @@ doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md
doc/UpdateAssetsToSharedLinkDto.md
doc/UpdateTagDto.md
doc/UpdateUserDto.md
doc/UpsertDeviceInfoDto.md
@@ -189,7 +188,6 @@ lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart
lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart
lib/model/update_assets_to_shared_link_dto.dart
lib/model/update_tag_dto.dart
lib/model/update_user_dto.dart
lib/model/upsert_device_info_dto.dart
@@ -281,7 +279,6 @@ test/thumbnail_format_test.dart
test/time_group_enum_test.dart
test/update_album_dto_test.dart
test/update_asset_dto_test.dart
test/update_assets_to_shared_link_dto_test.dart
test/update_tag_dto_test.dart
test/update_user_dto_test.dart
test/upsert_device_info_dto_test.dart

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.45.0
- API version: 1.47.2
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -75,6 +75,7 @@ Class | Method | HTTP request | Description
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
*AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
*AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
@@ -92,10 +93,10 @@ Class | Method | HTTP request | Description
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{assetId} |
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{assetId} |
*AssetApi* | [**updateAssetsInSharedLink**](doc//AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link |
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
*AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
@@ -214,7 +215,6 @@ Class | Method | HTTP request | Description
- [TimeGroupEnum](doc//TimeGroupEnum.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateAssetsToSharedLinkDto](doc//UpdateAssetsToSharedLinkDto.md)
- [UpdateTagDto](doc//UpdateTagDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md)
- [UpsertDeviceInfoDto](doc//UpsertDeviceInfoDto.md)

View File

@@ -9,6 +9,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
[**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
@@ -26,13 +27,62 @@ Method | HTTP request | Description
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{assetId} |
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{assetId} |
[**updateAssetsInSharedLink**](AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link |
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
# **addAssetsToSharedLink**
> SharedLinkResponseDto addAssetsToSharedLink(addAssetsDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final addAssetsDto = AddAssetsDto(); // AddAssetsDto |
try {
final result = api_instance.addAssetsToSharedLink(addAssetsDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->addAssetsToSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.md)| |
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **checkDuplicateAsset**
> CheckDuplicateAssetResponseDto checkDuplicateAsset(checkDuplicateAssetDto)
@@ -856,6 +906,55 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **removeAssetsFromSharedLink**
> SharedLinkResponseDto removeAssetsFromSharedLink(removeAssetsDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |
try {
final result = api_instance.removeAssetsFromSharedLink(removeAssetsDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->removeAssetsFromSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.md)| |
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **searchAsset**
> List<AssetResponseDto> searchAsset(searchAssetDto)
@@ -1009,57 +1108,8 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **updateAssetsInSharedLink**
> SharedLinkResponseDto updateAssetsInSharedLink(updateAssetsToSharedLinkDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final updateAssetsToSharedLinkDto = UpdateAssetsToSharedLinkDto(); // UpdateAssetsToSharedLinkDto |
try {
final result = api_instance.updateAssetsInSharedLink(updateAssetsToSharedLinkDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->updateAssetsInSharedLink: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**updateAssetsToSharedLinkDto** | [**UpdateAssetsToSharedLinkDto**](UpdateAssetsToSharedLinkDto.md)| |
### Return type
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **uploadFile**
> AssetFileUploadResponseDto uploadFile(assetData)
> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration)
@@ -1076,10 +1126,20 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final assetType = ; // AssetTypeEnum |
final assetData = BINARY_DATA_HERE; // MultipartFile |
final deviceAssetId = deviceAssetId_example; // String |
final deviceId = deviceId_example; // String |
final createdAt = createdAt_example; // String |
final modifiedAt = modifiedAt_example; // String |
final isFavorite = true; // bool |
final fileExtension = fileExtension_example; // String |
final livePhotoData = BINARY_DATA_HERE; // MultipartFile |
final isVisible = true; // bool |
final duration = duration_example; // String |
try {
final result = api_instance.uploadFile(assetData);
final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration);
print(result);
} catch (e) {
print('Exception when calling AssetApi->uploadFile: $e\n');
@@ -1090,7 +1150,17 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md)| |
**assetData** | **MultipartFile**| |
**deviceAssetId** | **String**| |
**deviceId** | **String**| |
**createdAt** | **String**| |
**modifiedAt** | **String**| |
**isFavorite** | **bool**| |
**fileExtension** | **String**| |
**livePhotoData** | **MultipartFile**| | [optional]
**isVisible** | **bool**| | [optional]
**duration** | **String**| | [optional]
### Return type

View File

@@ -1,15 +0,0 @@
# openapi.model.UpdateAssetsToSharedLinkDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetIds** | **List<String>** | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -14,6 +14,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:intl/intl.dart';
import 'package:meta/meta.dart';
@@ -112,7 +113,6 @@ part 'model/thumbnail_format.dart';
part 'model/time_group_enum.dart';
part 'model/update_album_dto.dart';
part 'model/update_asset_dto.dart';
part 'model/update_assets_to_shared_link_dto.dart';
part 'model/update_tag_dto.dart';
part 'model/update_user_dto.dart';
part 'model/upsert_device_info_dto.dart';

View File

@@ -16,6 +16,58 @@ class AssetApi {
final ApiClient apiClient;
///
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [AddAssetsDto] addAssetsDto (required):
Future<Response> addAssetsToSharedLinkWithHttpInfo(AddAssetsDto addAssetsDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/shared-link/add';
// ignore: prefer_final_locals
Object? postBody = addAssetsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
///
///
/// Parameters:
///
/// * [AddAssetsDto] addAssetsDto (required):
Future<SharedLinkResponseDto?> addAssetsToSharedLink(AddAssetsDto addAssetsDto,) async {
final response = await addAssetsToSharedLinkWithHttpInfo(addAssetsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
/// Check duplicated asset before uploading - for Web upload used
///
/// Note: This method returns the HTTP [Response].
@@ -926,6 +978,58 @@ class AssetApi {
return null;
}
///
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [RemoveAssetsDto] removeAssetsDto (required):
Future<Response> removeAssetsFromSharedLinkWithHttpInfo(RemoveAssetsDto removeAssetsDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/shared-link/remove';
// ignore: prefer_final_locals
Object? postBody = removeAssetsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
///
///
/// Parameters:
///
/// * [RemoveAssetsDto] removeAssetsDto (required):
Future<SharedLinkResponseDto?> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto,) async {
final response = await removeAssetsFromSharedLinkWithHttpInfo(removeAssetsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
///
///
/// Note: This method returns the HTTP [Response].
@@ -1112,60 +1216,28 @@ class AssetApi {
///
/// Parameters:
///
/// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required):
Future<Response> updateAssetsInSharedLinkWithHttpInfo(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/shared-link';
// ignore: prefer_final_locals
Object? postBody = updateAssetsToSharedLinkDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PATCH',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
///
///
/// Parameters:
///
/// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required):
Future<SharedLinkResponseDto?> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async {
final response = await updateAssetsInSharedLinkWithHttpInfo(updateAssetsToSharedLinkDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
}
return null;
}
///
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
/// * [AssetTypeEnum] assetType (required):
///
/// * [MultipartFile] assetData (required):
Future<Response> uploadFileWithHttpInfo(MultipartFile assetData,) async {
///
/// * [String] deviceAssetId (required):
///
/// * [String] deviceId (required):
///
/// * [String] createdAt (required):
///
/// * [String] modifiedAt (required):
///
/// * [bool] isFavorite (required):
///
/// * [String] fileExtension (required):
///
/// * [MultipartFile] livePhotoData:
///
/// * [bool] isVisible:
///
/// * [String] duration:
Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/upload';
@@ -1180,11 +1252,52 @@ class AssetApi {
bool hasFields = false;
final mp = MultipartRequest('POST', Uri.parse(path));
if (assetType != null) {
hasFields = true;
mp.fields[r'assetType'] = parameterToString(assetType);
}
if (assetData != null) {
hasFields = true;
mp.fields[r'assetData'] = assetData.field;
mp.files.add(assetData);
}
if (livePhotoData != null) {
hasFields = true;
mp.fields[r'livePhotoData'] = livePhotoData.field;
mp.files.add(livePhotoData);
}
if (deviceAssetId != null) {
hasFields = true;
mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
}
if (deviceId != null) {
hasFields = true;
mp.fields[r'deviceId'] = parameterToString(deviceId);
}
if (createdAt != null) {
hasFields = true;
mp.fields[r'createdAt'] = parameterToString(createdAt);
}
if (modifiedAt != null) {
hasFields = true;
mp.fields[r'modifiedAt'] = parameterToString(modifiedAt);
}
if (isFavorite != null) {
hasFields = true;
mp.fields[r'isFavorite'] = parameterToString(isFavorite);
}
if (isVisible != null) {
hasFields = true;
mp.fields[r'isVisible'] = parameterToString(isVisible);
}
if (fileExtension != null) {
hasFields = true;
mp.fields[r'fileExtension'] = parameterToString(fileExtension);
}
if (duration != null) {
hasFields = true;
mp.fields[r'duration'] = parameterToString(duration);
}
if (hasFields) {
postBody = mp;
}
@@ -1204,9 +1317,29 @@ class AssetApi {
///
/// Parameters:
///
/// * [AssetTypeEnum] assetType (required):
///
/// * [MultipartFile] assetData (required):
Future<AssetFileUploadResponseDto?> uploadFile(MultipartFile assetData,) async {
final response = await uploadFileWithHttpInfo(assetData,);
///
/// * [String] deviceAssetId (required):
///
/// * [String] deviceId (required):
///
/// * [String] createdAt (required):
///
/// * [String] modifiedAt (required):
///
/// * [bool] isFavorite (required):
///
/// * [String] fileExtension (required):
///
/// * [MultipartFile] livePhotoData:
///
/// * [bool] isVisible:
///
/// * [String] duration:
Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData: livePhotoData, isVisible: isVisible, duration: duration, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -144,19 +144,19 @@ class ApiClient {
);
}
Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) async =>
Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) =>
// ignore: deprecated_member_use_from_same_package
deserialize(json, targetType, growable: growable);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
dynamic deserialize(String json, String targetType, {bool growable = false,}) {
Future<dynamic> deserialize(String json, String targetType, {bool growable = false,}) async {
// Remove all spaces. Necessary for regular expressions as well.
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
// If the expected target type is String, nothing to do...
return targetType == 'String'
? json
: _deserialize(jsonDecode(json), targetType, growable: growable);
: _deserialize(await compute((String j) => jsonDecode(j), json), targetType, growable: growable);
}
// ignore: deprecated_member_use_from_same_package
@@ -336,8 +336,6 @@ class ApiClient {
return UpdateAlbumDto.fromJson(value);
case 'UpdateAssetDto':
return UpdateAssetDto.fromJson(value);
case 'UpdateAssetsToSharedLinkDto':
return UpdateAssetsToSharedLinkDto.fromJson(value);
case 'UpdateTagDto':
return UpdateTagDto.fromJson(value);
case 'UpdateUserDto':

View File

@@ -1,113 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class UpdateAssetsToSharedLinkDto {
/// Returns a new [UpdateAssetsToSharedLinkDto] instance.
UpdateAssetsToSharedLinkDto({
this.assetIds = const [],
});
List<String> assetIds;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetsToSharedLinkDto &&
other.assetIds == assetIds;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetIds.hashCode);
@override
String toString() => 'UpdateAssetsToSharedLinkDto[assetIds=$assetIds]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'assetIds'] = this.assetIds;
return json;
}
/// Returns a new [UpdateAssetsToSharedLinkDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UpdateAssetsToSharedLinkDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "UpdateAssetsToSharedLinkDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "UpdateAssetsToSharedLinkDto[$key]" has a null value in JSON.');
});
return true;
}());
return UpdateAssetsToSharedLinkDto(
assetIds: json[r'assetIds'] is List
? (json[r'assetIds'] as List).cast<String>()
: const [],
);
}
return null;
}
static List<UpdateAssetsToSharedLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <UpdateAssetsToSharedLinkDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UpdateAssetsToSharedLinkDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UpdateAssetsToSharedLinkDto> mapFromJson(dynamic json) {
final map = <String, UpdateAssetsToSharedLinkDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UpdateAssetsToSharedLinkDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UpdateAssetsToSharedLinkDto-objects as value to a dart map
static Map<String, List<UpdateAssetsToSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UpdateAssetsToSharedLinkDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UpdateAssetsToSharedLinkDto.listFromJson(entry.value, growable: growable,);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetIds',
};
}

View File

@@ -17,6 +17,13 @@ void main() {
// final instance = AssetApi();
group('tests for AssetApi', () {
//
//
//Future<SharedLinkResponseDto> addAssetsToSharedLink(AddAssetsDto addAssetsDto) async
test('test addAssetsToSharedLink', () async {
// TODO
});
// Check duplicated asset before uploading - for Web upload used
//
//Future<CheckDuplicateAssetResponseDto> checkDuplicateAsset(CheckDuplicateAssetDto checkDuplicateAssetDto) async
@@ -136,6 +143,13 @@ void main() {
// TODO
});
//
//
//Future<SharedLinkResponseDto> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto) async
test('test removeAssetsFromSharedLink', () async {
// TODO
});
//
//
//Future<List<AssetResponseDto>> searchAsset(SearchAssetDto searchAssetDto) async
@@ -159,14 +173,7 @@ void main() {
//
//
//Future<SharedLinkResponseDto> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto) async
test('test updateAssetsInSharedLink', () async {
// TODO
});
//
//
//Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData) async
//Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile livePhotoData, bool isVisible, String duration }) async
test('test uploadFile', () async {
// TODO
});

View File

@@ -1,27 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for UpdateAssetsToSharedLinkDto
void main() {
// final instance = UpdateAssetsToSharedLinkDto();
group('test UpdateAssetsToSharedLinkDto', () {
// List<String> assetIds (default value: const [])
test('to test the property `assetIds`', () async {
// TODO
});
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.46.0+69
version: 1.47.0+70
isar_version: &isar_version 3.0.5
environment:

View File

@@ -0,0 +1,44 @@
#!/bin/sh
# vim:sw=4:ts=4:et
set -e
entrypoint_log() {
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
echo "$@"
fi
}
ME=$(basename $0)
DEFAULT_CONF_FILE="etc/nginx/conf.d/default.conf"
# check if we have ipv6 available
if [ ! -f "/proc/net/if_inet6" ]; then
entrypoint_log "$ME: info: ipv6 not available"
exit 0
fi
if [ ! -f "/$DEFAULT_CONF_FILE" ]; then
entrypoint_log "$ME: info: /$DEFAULT_CONF_FILE is not a file or does not exist"
exit 0
fi
# check if the file can be modified, e.g. not on a r/o filesystem
touch /$DEFAULT_CONF_FILE 2>/dev/null || { entrypoint_log "$ME: info: can not modify /$DEFAULT_CONF_FILE (read-only file system?)"; exit 0; }
# check if the file is already modified, e.g. on a container restart
grep -q "listen \[::]\:8080;" /$DEFAULT_CONF_FILE && { entrypoint_log "$ME: info: IPv6 listen already enabled"; exit 0; }
if [ -f "/etc/os-release" ]; then
. /etc/os-release
else
entrypoint_log "$ME: info: can not guess the operating system"
exit 0
fi
# enable ipv6 on default.conf listen sockets
sed -i -E 's,listen 8080;,listen 8080;\n listen [::]:8080;,' /$DEFAULT_CONF_FILE
entrypoint_log "$ME: info: Enabled listen on IPv6 in /$DEFAULT_CONF_FILE"
exit 0

View File

@@ -1,4 +1,4 @@
#! /bin/sh
#!/bin/sh
set -e
export IMMICH_WEB_URL="${IMMICH_WEB_URL:-http://immich-web:3000}"
@@ -11,7 +11,3 @@ IMMICH_SERVER_SCHEME=$(echo "$IMMICH_WEB_URL" | grep -Eo '^https?://' || echo "h
export IMMICH_SERVER_SCHEME
IMMICH_SERVER_HOST=$(echo "$IMMICH_SERVER_URL" | cut -d '/' -f 3)
export IMMICH_SERVER_HOST
envsubst '$IMMICH_WEB_SCHEME $IMMICH_WEB_HOST $IMMICH_SERVER_SCHEME $IMMICH_SERVER_HOST' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
exec nginx -g 'daemon off;'

View File

@@ -3,9 +3,7 @@ FROM docker.io/nginxinc/nginx-unprivileged:latest
COPY LICENSE /licenses/LICENSE.txt
COPY LICENSE /LICENSE
COPY nginx.conf "/etc/nginx/nginx.conf.template"
COPY start.sh /start.sh
COPY 10-listen-on-ipv6-by-default.sh /docker-entrypoint.d
COPY 15-set-env-variables.envsh /docker-entrypoint.d
STOPSIGNAL SIGQUIT
ENTRYPOINT ["/start.sh"]
COPY templates/ /etc/nginx/templates

View File

@@ -1,104 +0,0 @@
# NOTE: This file is generated on startup. See /start.sh
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /tmp/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
# events {
# worker_connections 1000;
# }
upstream server {
server $IMMICH_SERVER_HOST;
keepalive 2;
}
upstream web {
server $IMMICH_WEB_HOST;
keepalive 2;
}
server {
# Compression
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_proxied any;
gzip_types
application/javascript
application/json
font/truetype
image/svg+xml
text/css
text/html;
gzip_vary on;
gunzip on;
client_max_body_size 50000M;
listen 8080;
listen [::]:8080;
access_log off;
location /api {
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
rewrite /api/(.*) /$1 break;
proxy_pass ${IMMICH_SERVER_SCHEME}server;
}
location / {
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_pass ${IMMICH_WEB_SCHEME}web;
}
}
}

View File

@@ -0,0 +1,77 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream server {
server ${IMMICH_SERVER_HOST};
keepalive 2;
}
upstream web {
server ${IMMICH_WEB_HOST};
keepalive 2;
}
server {
listen 8080;
access_log off;
client_max_body_size 50000M;
# Compression
gzip off;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_proxied any;
gzip_vary on;
gunzip on;
# text/html is included by default
gzip_types
application/javascript
application/json
font/ttf
image/svg+xml
text/css;
location /api {
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
rewrite /api/(.*) /$1 break;
proxy_pass ${IMMICH_SERVER_SCHEME}server;
}
location / {
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_pass ${IMMICH_WEB_SCHEME}web;
}
}

View File

@@ -1,7 +1,7 @@
import { AlbumService } from './album.service';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { AlbumEntity, UserEntity } from '@app/infra';
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra';
import { AlbumResponseDto, ICryptoRepository, mapUser } from '@app/domain';
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
import { IAlbumRepository } from './album-repository';
@@ -513,4 +513,71 @@ describe('Album service', () => {
expect(result).toHaveLength(1);
expect(result[0].assetCount).toEqual(1);
});
it('updates the album thumbnail by listing all albums', async () => {
const albumEntity = _getOwnedAlbum();
const assetEntity = new AssetEntity();
const newThumbnailAssetId = 'e5e65c02-b889-4f3c-afe1-a39a96d578ed';
albumEntity.albumThumbnailAssetId = 'nonexistent';
assetEntity.id = newThumbnailAssetId;
albumEntity.assets = [
{
id: '760841c1-f7c4-42b1-96af-c7d007a26126',
assetId: assetEntity.id,
albumId: albumEntity.id,
albumInfo: albumEntity,
assetInfo: assetEntity,
},
];
albumRepositoryMock.getList.mockImplementation(async () => [albumEntity]);
albumRepositoryMock.updateAlbum.mockImplementation(async () => ({
...albumEntity,
albumThumbnailAssetId: newThumbnailAssetId,
}));
const result = await sut.getAllAlbums(authUser, {});
expect(result).toHaveLength(1);
expect(result[0].albumThumbnailAssetId).toEqual(newThumbnailAssetId);
expect(albumRepositoryMock.getList).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.getList).toHaveBeenCalledWith(albumEntity.ownerId, {});
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
albumThumbnailAssetId: newThumbnailAssetId,
});
});
it('removes the thumbnail for an empty album', async () => {
const albumEntity = _getOwnedAlbum();
const newAlbumEntity = { ...albumEntity, albumThumbnailAssetId: null };
albumEntity.albumThumbnailAssetId = 'e5e65c02-b889-4f3c-afe1-a39a96d578ed';
albumRepositoryMock.getList.mockImplementation(async () => [albumEntity]);
albumRepositoryMock.updateAlbum.mockImplementation(async () => newAlbumEntity);
const result = await sut.getAllAlbums(authUser, {});
expect(result).toHaveLength(1);
expect(result[0].albumThumbnailAssetId).toBeNull();
expect(albumRepositoryMock.getList).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.getList).toHaveBeenCalledWith(albumEntity.ownerId, {});
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(newAlbumEntity, {
albumThumbnailAssetId: undefined,
});
});
it('listing empty albums does not unnecessarily update the album', async () => {
const albumEntity = _getOwnedAlbum();
albumRepositoryMock.getList.mockImplementation(async () => [albumEntity]);
albumRepositoryMock.updateAlbum.mockImplementation(async () => albumEntity);
const result = await sut.getAllAlbums(authUser, {});
expect(result).toHaveLength(1);
expect(albumRepositoryMock.getList).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(0);
expect(albumRepositoryMock.getList).toHaveBeenCalledWith(albumEntity.ownerId, {});
});
});

View File

@@ -187,15 +187,19 @@ export class AlbumService {
async _checkValidThumbnail(album: AlbumEntity) {
const assets = album.assets || [];
const valid = assets.some((asset) => asset.assetId === album.albumThumbnailAssetId);
if (!valid) {
let dto: UpdateAlbumDto = {};
if (assets.length > 0) {
const albumThumbnailAssetId = assets[0].assetId;
dto = { albumThumbnailAssetId };
}
await this._albumRepository.updateAlbum(album, dto);
album.albumThumbnailAssetId = dto.albumThumbnailAssetId || null;
// Check if the album's thumbnail is invalid by referencing
// an asset outside the album.
const invalid = assets.length > 0 && !assets.some((asset) => asset.assetId === album.albumThumbnailAssetId);
// Check if an empty album still has a thumbnail.
const isEmptyWithThumbnail = assets.length === 0 && album.albumThumbnailAssetId !== null;
if (invalid || isEmptyWithThumbnail) {
const albumThumbnailAssetId = assets[0]?.assetId;
album.albumThumbnailAssetId = albumThumbnailAssetId || null;
await this._albumRepository.updateAlbum(album, { albumThumbnailAssetId });
}
}

View File

@@ -1,3 +1,4 @@
import { AddAssetsDto } from './../album/dto/add-assets.dto';
import {
Controller,
Post,
@@ -16,6 +17,7 @@ import {
UploadedFiles,
Patch,
StreamableFile,
ParseFilePipe,
} from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service';
@@ -31,7 +33,6 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetResponseDto, ImmichReadStream } from '@app/domain';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
import { DeleteAssetResponseDto } from './response-dto/delete-asset-response.dto';
@@ -52,9 +53,10 @@ import {
import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { SharedLinkResponseDto } from '@app/domain';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
import { AssetSearchDto } from './dto/asset-search.dto';
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
return new StreamableFile(stream, { type, length });
@@ -80,12 +82,13 @@ export class AssetController {
@ApiConsumes('multipart/form-data')
@ApiBody({
description: 'Asset Upload Information',
type: AssetFileUploadDto,
type: CreateAssetDto,
})
async uploadFile(
@GetAuthUser() authUser: AuthUserDto,
@UploadedFiles() files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] },
@Body(ValidationPipe) dto: CreateAssetDto,
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] }))
files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] },
@Body(new ValidationPipe()) dto: CreateAssetDto,
@Response({ passthrough: true }) res: Res,
): Promise<AssetFileUploadResponseDto> {
const file = mapToUploadFile(files.assetData[0]);
@@ -328,11 +331,20 @@ export class AssetController {
}
@Authenticated({ isShared: true })
@Patch('/shared-link')
async updateAssetsInSharedLink(
@Patch('/shared-link/add')
async addAssetsToSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
@Body(ValidationPipe) dto: AddAssetsDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.updateAssetsInSharedLink(authUser, dto);
return await this.assetService.addAssetsToSharedLink(authUser, dto);
}
@Authenticated({ isShared: true })
@Patch('/shared-link/remove')
async removeAssetsFromSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: RemoveAssetsDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.removeAssetsFromSharedLink(authUser, dto);
}
}

View File

@@ -198,14 +198,31 @@ describe('AssetService', () => {
sharedLinkRepositoryMock.get.mockResolvedValue(null);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
await expect(sut.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
});
it('should add assets to a shared link', async () => {
const asset1 = _getAsset_1();
const authDto = authStub.adminSharedLink;
const dto = { assetIds: [asset1.id] };
assetRepositoryMock.getById.mockResolvedValue(asset1);
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
});
it('should remove assets from a shared link', async () => {
const asset1 = _getAsset_1();
@@ -217,11 +234,11 @@ describe('AssetService', () => {
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
});
});

View File

@@ -58,8 +58,9 @@ import { ISharedLinkRepository } from '@app/domain';
import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
import { AssetSearchDto } from './dto/asset-search.dto';
import { AddAssetsDto } from '../album/dto/add-assets.dto';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
const fileInfo = promisify(stat);
@@ -606,23 +607,35 @@ export class AssetService {
return mapSharedLink(sharedLink);
}
async updateAssetsInSharedLink(
authUser: AuthUserDto,
dto: UpdateAssetsToSharedLinkDto,
): Promise<SharedLinkResponseDto> {
async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) {
throw new ForbiddenException();
}
const assets = [];
await this.checkAssetsAccess(authUser, dto.assetIds);
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const updatedLink = await this.shareCore.updateAssets(authUser.id, authUser.sharedLinkId, assets);
const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets);
return mapSharedLink(updatedLink);
}
async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) {
throw new ForbiddenException();
}
const assets = [];
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets);
return mapSharedLink(updatedLink);
}

View File

@@ -1,6 +0,0 @@
import { IsNotEmpty } from 'class-validator';
export class UpdateAssetsToSharedLinkDto {
@IsNotEmpty()
assetIds!: string[];
}

View File

@@ -1,8 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class AssetFileUploadDto {
@IsNotEmpty()
@ApiProperty({ type: 'string', format: 'binary' })
assetData!: any;
}

View File

@@ -1,6 +1,6 @@
import { AssetType } from '@app/infra';
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { ImmichFile } from '../../../config/asset-upload.config';
export class CreateAssetDto {
@@ -11,6 +11,7 @@ export class CreateAssetDto {
deviceId!: string;
@IsNotEmpty()
@IsEnum(AssetType)
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
assetType!: AssetType;
@@ -32,6 +33,14 @@ export class CreateAssetDto {
@IsOptional()
duration?: string;
// The properties below are added to correctly generate the API docs
// and client SDKs. Validation should be handled in the controller.
@ApiProperty({ type: 'string', format: 'binary' })
assetData!: any;
@ApiProperty({ type: 'string', format: 'binary' })
livePhotoData?: any;
}
export interface UploadFile {

View File

@@ -0,0 +1,25 @@
import { FileValidator, Injectable } from '@nestjs/common';
@Injectable()
export default class FileNotEmptyValidator extends FileValidator {
requiredFields: string[];
constructor(requiredFields: string[]) {
super({});
this.requiredFields = requiredFields;
}
isValid(files?: any): boolean {
if (!files) {
return false;
}
return this.requiredFields.every((field) => {
return files[field];
});
}
buildErrorMessage(): string {
return `Field(s) ${this.requiredFields.join(', ')} should not be empty`;
}
}

View File

@@ -55,7 +55,7 @@ describe('assetUploadOption', () => {
});
it('should allow videos', async () => {
const file = { mimetype: 'image/mp4', originalname: 'test.mp4' } as any;
const file = { mimetype: 'video/mp4', originalname: 'test.mp4' } as any;
fileFilter(mock.userRequest, file, callback);
expect(callback).toHaveBeenCalledWith(null, true);
});
@@ -66,6 +66,18 @@ describe('assetUploadOption', () => {
expect(callback).toHaveBeenCalledWith(null, true);
});
it('should allow .raf recognized', () => {
const file = { mimetype: 'image/x-fuji-raf', originalname: 'test.raf' } as any;
fileFilter(mock.userRequest, file, callback);
expect(callback).toHaveBeenCalledWith(null, true);
});
it('should allow .srw recognized', () => {
const file = { mimetype: 'image/x-samsung-srw', originalname: 'test.srw' } as any;
fileFilter(mock.userRequest, file, callback);
expect(callback).toHaveBeenCalledWith(null, true);
});
it('should not allow unknown types', async () => {
const file = { mimetype: 'application/html', originalname: 'test.html' } as any;
const callback = jest.fn();

View File

@@ -10,8 +10,6 @@ import sanitize from 'sanitize-filename';
import { AuthUserDto } from '../decorators/auth-user.decorator';
import { patchFormData } from '../utils/path-form-data.util';
const logger = new Logger('AssetUploadConfig');
export interface ImmichFile extends Express.Multer.File {
/** sha1 hash of file */
checksum: Buffer;
@@ -48,13 +46,15 @@ export function customStorage(): StorageEngine {
export const multerUtils = { fileFilter, filename, destination };
const logger = new Logger('AssetUploadConfig');
function fileFilter(req: Request, file: any, cb: any) {
if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) {
return cb(new UnauthorizedException());
}
if (
file.mimetype.match(
/\/(jpg|jpeg|png|gif|mp4|webm|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp|tiff|3gpp|nef|x-nikon-nef)$/,
/\/(jpg|jpeg|png|gif|mp4|webm|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp|tiff|3gpp|nef|x-nikon-nef|x-fuji-raf|x-samsung-srw)$/,
)
) {
cb(null, true);

View File

@@ -1,5 +1,14 @@
import { immichAppConfig } from '@app/common/config';
import { AssetEntity, ExifEntity, SmartInfoEntity, UserEntity, APIKeyEntity, InfraModule } from '@app/infra';
import {
AssetEntity,
ExifEntity,
SmartInfoEntity,
UserEntity,
APIKeyEntity,
InfraModule,
UserTokenEntity,
AlbumEntity,
} from '@app/infra';
import { StorageModule } from '@app/storage';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@@ -23,7 +32,15 @@ import { DomainModule } from '@app/domain';
DomainModule.register({
imports: [InfraModule],
}),
TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity, APIKeyEntity]),
TypeOrmModule.forFeature([
UserEntity,
ExifEntity,
AssetEntity,
SmartInfoEntity,
APIKeyEntity,
UserTokenEntity,
AlbumEntity,
]),
StorageModule,
CommunicationModule,
],

View File

@@ -14,6 +14,7 @@ import { Repository } from 'typeorm/repository/Repository';
import { join } from 'path';
import { CommunicationGateway } from 'apps/immich/src/api-v1/communication/communication.gateway';
import { IMachineLearningJob } from '@app/domain';
import { exiftool } from 'exiftool-vendored';
@Processor(QueueName.THUMBNAIL_GENERATION)
export class ThumbnailGeneratorProcessor {
@@ -50,10 +51,18 @@ export class ThumbnailGeneratorProcessor {
if (asset.type == AssetType.IMAGE) {
try {
await sharp(asset.originalPath, { failOnError: false })
.resize(1440, 2560, { fit: 'inside' })
.resize(1440, 1440, { fit: 'outside', withoutEnlargement: true })
.jpeg()
.rotate()
.toFile(jpegThumbnailPath);
.toFile(jpegThumbnailPath)
.catch(() => {
this.logger.warn(
'Failed to generate jpeg thumbnail for asset: ' +
asset.id +
' using sharp, failing over to exiftool-vendored',
);
return exiftool.extractThumbnail(asset.originalPath, jpegThumbnailPath);
});
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
} catch (error: any) {
this.logger.error('Failed to generate jpeg thumbnail for asset: ' + asset.id, error.stack);

View File

@@ -1,5 +1,5 @@
import { APP_UPLOAD_LOCATION, userUtils } from '@app/common';
import { APIKeyEntity, AssetEntity, UserEntity } from '@app/infra';
import { AlbumEntity, APIKeyEntity, AssetEntity, UserEntity, UserTokenEntity } from '@app/infra';
import { QueueName, JobName } from '@app/domain';
import { IUserDeletionJob } from '@app/domain';
import { Process, Processor } from '@nestjs/bull';
@@ -23,6 +23,12 @@ export class UserDeletionProcessor {
@InjectRepository(APIKeyEntity)
private apiKeyRepository: Repository<APIKeyEntity>,
@InjectRepository(UserTokenEntity)
private userTokenRepository: Repository<UserTokenEntity>,
@InjectRepository(AlbumEntity)
private albumRepository: Repository<AlbumEntity>,
) {}
@Process(JobName.USER_DELETION)
@@ -44,6 +50,16 @@ export class UserDeletionProcessor {
fs.rmSync(userAssetDir, { recursive: true, force: true });
this.logger.warn(`Removing user from database: ${user.id}`);
const userTokens = await this.userTokenRepository.find({
where: { user: { id: user.id } },
relations: { user: true },
withDeleted: true,
});
await this.userTokenRepository.remove(userTokens);
const albums = await this.albumRepository.find({ where: { ownerId: user.id } });
await this.albumRepository.remove(albums);
await this.apiKeyRepository.delete({ userId: user.id });
await this.assetRepository.delete({ userId: user.id });
await this.userRepository.remove(user);

View File

@@ -1,17 +1,25 @@
#!/bin/bash
#!/usr/bin/env bash
function mobile {
rm -rf ../mobile/openapi
cd ./openapi-generator/templates/serialization/native
cd ./openapi-generator/templates/mobile/serialization/native
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/master/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
patch -u native_class.mustache <native_class.mustache.patch
cd ../../../..
npx openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./openapi-generator/templates
cd ../../../../..
npx openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./openapi-generator/templates/mobile
# Post generate patches
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./openapi-generator/patch/api_client.dart.patch
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./openapi-generator/patch/api.dart.patch
}
function web {
rm -rf ../web/src/api/open-api
npx openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/api/open-api
cd ./openapi-generator/templates/web
wget -O apiInner.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/v6.0.1/modules/openapi-generator/src/main/resources/typescript-axios/apiInner.mustache
patch -u apiInner.mustache < apiInner.mustache.patch
cd ../../..
npx openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/api/open-api -t ./openapi-generator/templates/web
}
if [[ $1 == 'mobile' ]]; then

View File

@@ -1077,7 +1077,7 @@
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/AssetFileUploadDto"
"$ref": "#/components/schemas/CreateAssetDto"
}
}
}
@@ -1869,9 +1869,11 @@
"bearer": []
}
]
},
}
},
"/asset/shared-link/add": {
"patch": {
"operationId": "updateAssetsInSharedLink",
"operationId": "addAssetsToSharedLink",
"description": "",
"parameters": [],
"requestBody": {
@@ -1879,7 +1881,44 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto"
"$ref": "#/components/schemas/AddAssetsDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharedLinkResponseDto"
}
}
}
}
},
"tags": [
"Asset"
],
"security": [
{
"bearer": []
}
]
}
},
"/asset/shared-link/remove": {
"patch": {
"operationId": "removeAssetsFromSharedLink",
"description": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RemoveAssetsDto"
}
}
}
@@ -2689,7 +2728,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.46.0",
"version": "1.47.3",
"contact": {}
},
"tags": [],
@@ -3758,16 +3797,54 @@
"profileImagePath"
]
},
"AssetFileUploadDto": {
"CreateAssetDto": {
"type": "object",
"properties": {
"assetType": {
"$ref": "#/components/schemas/AssetTypeEnum"
},
"assetData": {
"type": "string",
"format": "binary"
},
"livePhotoData": {
"type": "string",
"format": "binary"
},
"deviceAssetId": {
"type": "string"
},
"deviceId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"modifiedAt": {
"type": "string"
},
"isFavorite": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
"fileExtension": {
"type": "string"
},
"duration": {
"type": "string"
}
},
"required": [
"assetData"
"assetType",
"assetData",
"deviceAssetId",
"deviceId",
"createdAt",
"modifiedAt",
"isFavorite",
"fileExtension"
]
},
"AssetFileUploadResponseDto": {
@@ -4133,7 +4210,21 @@
"assetIds"
]
},
"UpdateAssetsToSharedLinkDto": {
"AddAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"RemoveAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
@@ -4229,20 +4320,6 @@
"sharedUserIds"
]
},
"AddAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"AddAssetsResponseDto": {
"type": "object",
"properties": {
@@ -4264,20 +4341,6 @@
"alreadyInAlbum"
]
},
"RemoveAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"UpdateAlbumDto": {
"type": "object",
"properties": {

View File

@@ -63,13 +63,24 @@ export class ShareCore {
return this.repository.remove(link);
}
async updateAssets(userId: string, id: string, assets: AssetEntity[]) {
async addAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
return this.repository.save({ ...link, assets });
return this.repository.save({ ...link, assets: [...link.assets, ...assets] });
}
async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
return this.repository.save({ ...link, assets: newAssets });
}
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {

View File

@@ -4,6 +4,7 @@ export class AddAlbumUserForeignKeyConstraint1675701909594 implements MigrationI
name = 'AddAlbumUserForeignKeyConstraint1675701909594';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM "albums" WHERE "ownerId"::uuid NOT IN (SELECT id FROM "users") `)
await queryRunner.query(`ALTER TABLE "albums" ALTER COLUMN "ownerId" TYPE varchar(36)`);
await queryRunner.query(`ALTER TABLE "albums" ALTER COLUMN "ownerId" TYPE uuid using "ownerId"::uuid`);
await queryRunner.query(

View File

@@ -0,0 +1,8 @@
@@ -14,6 +14,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:intl/intl.dart';
import 'package:meta/meta.dart';

View File

@@ -0,0 +1,21 @@
@@ -144,19 +144,19 @@ class ApiClient {
);
}
- Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) async =>
+ Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) =>
// ignore: deprecated_member_use_from_same_package
deserialize(json, targetType, growable: growable);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
- dynamic deserialize(String json, String targetType, {bool growable = false,}) {
+ Future<dynamic> deserialize(String json, String targetType, {bool growable = false,}) async {
// Remove all spaces. Necessary for regular expressions as well.
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
// If the expected target type is String, nothing to do...
return targetType == 'String'
? json
- : _deserialize(jsonDecode(json), targetType, growable: growable);
+ : _deserialize(await compute((String j) => jsonDecode(j), json), targetType, growable: growable);
}

View File

@@ -0,0 +1,372 @@
{{#withSeparateModelsAndApi}}
/* tslint:disable */
/* eslint-disable */
{{>licenseInfo}}
import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import { Configuration } from '{{apiRelativeToRoot}}configuration';
{{#withNodeImports}}
// URLSearchParams not necessarily used
// @ts-ignore
import { URL, URLSearchParams } from 'url';
{{#multipartFormData}}
import FormData from 'form-data'
{{/multipartFormData}}
{{/withNodeImports}}
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '{{apiRelativeToRoot}}common';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '{{apiRelativeToRoot}}base';
{{#imports}}
// @ts-ignore
import { {{classname}} } from '{{apiRelativeToRoot}}{{tsModelPackage}}';
{{/imports}}
{{/withSeparateModelsAndApi}}
{{^withSeparateModelsAndApi}}
{{/withSeparateModelsAndApi}}
{{#operations}}
/**
* {{classname}} - axios parameter creator{{#description}}
* {{&description}}{{/description}}
* @export
*/
export const {{classname}}AxiosParamCreator = function (configuration?: Configuration) {
return {
{{#operation}}
/**
* {{&notes}}
{{#summary}}
* @summary {{&summary}}
{{/summary}}
{{#allParams}}
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
{{/allParams}}
* @param {*} [options] Override http request option.{{#isDeprecated}}
* @deprecated{{/isDeprecated}}
* @throws {RequiredError}
*/
{{nickname}}: async ({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
{{#allParams}}
{{#required}}
// verify required parameter '{{paramName}}' is not null or undefined
assertParamExists('{{nickname}}', '{{paramName}}', {{paramName}})
{{/required}}
{{/allParams}}
const localVarPath = `{{{path}}}`{{#pathParams}}
.replace(`{${"{{baseName}}"}}`, encodeURIComponent(String({{paramName}}))){{/pathParams}};
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: '{{httpMethod}}', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;{{#vendorExtensions}}{{#hasFormParams}}
const localVarFormParams = new {{^multipartFormData}}URLSearchParams(){{/multipartFormData}}{{#multipartFormData}}((configuration && configuration.formDataCtor) || FormData)(){{/multipartFormData}};{{/hasFormParams}}{{/vendorExtensions}}
{{#authMethods}}
// authentication {{name}} required
{{#isApiKey}}
{{#isKeyInHeader}}
await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration)
{{/isKeyInHeader}}
{{#isKeyInQuery}}
await setApiKeyToObject(localVarQueryParameter, "{{keyParamName}}", configuration)
{{/isKeyInQuery}}
{{/isApiKey}}
{{#isBasicBasic}}
// http basic authentication required
setBasicAuthToObject(localVarRequestOptions, configuration)
{{/isBasicBasic}}
{{#isBasicBearer}}
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
{{/isBasicBearer}}
{{#isOAuth}}
// oauth required
await setOAuthToObject(localVarHeaderParameter, "{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}], configuration)
{{/isOAuth}}
{{/authMethods}}
{{#queryParams}}
{{#isArray}}
if ({{paramName}}) {
{{#isCollectionFormatMulti}}
{{#uniqueItems}}
localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}});
{{/uniqueItems}}
{{^uniqueItems}}
localVarQueryParameter['{{baseName}}'] = {{paramName}};
{{/uniqueItems}}
{{/isCollectionFormatMulti}}
{{^isCollectionFormatMulti}}
{{#uniqueItems}}
localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}}).join(COLLECTION_FORMATS.{{collectionFormat}});
{{/uniqueItems}}
{{^uniqueItems}}
localVarQueryParameter['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}});
{{/uniqueItems}}
{{/isCollectionFormatMulti}}
}
{{/isArray}}
{{^isArray}}
if ({{paramName}} !== undefined) {
{{#isDateTime}}
localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
({{paramName}} as any).toISOString() :
{{paramName}};
{{/isDateTime}}
{{^isDateTime}}
{{#isDate}}
localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
({{paramName}} as any).toISOString().substr(0,10) :
{{paramName}};
{{/isDate}}
{{^isDate}}
localVarQueryParameter['{{baseName}}'] = {{paramName}};
{{/isDate}}
{{/isDateTime}}
}
{{/isArray}}
{{/queryParams}}
{{#headerParams}}
{{#isArray}}
if ({{paramName}}) {
{{#uniqueItems}}
let mapped = Array.from({{paramName}}).map(value => (<any>"{{{dataType}}}" !== "Set<string>") ? JSON.stringify(value) : (value || ""));
{{/uniqueItems}}
{{^uniqueItems}}
let mapped = {{paramName}}.map(value => (<any>"{{{dataType}}}" !== "Array<string>") ? JSON.stringify(value) : (value || ""));
{{/uniqueItems}}
localVarHeaderParameter['{{baseName}}'] = mapped.join(COLLECTION_FORMATS["{{collectionFormat}}"]);
}
{{/isArray}}
{{^isArray}}
if ({{paramName}} !== undefined && {{paramName}} !== null) {
{{#isString}}
localVarHeaderParameter['{{baseName}}'] = String({{paramName}});
{{/isString}}
{{^isString}}
localVarHeaderParameter['{{baseName}}'] = String(JSON.stringify({{paramName}}));
{{/isString}}
}
{{/isArray}}
{{/headerParams}}
{{#vendorExtensions}}
{{#formParams}}
{{#isArray}}
if ({{paramName}}) {
{{#isCollectionFormatMulti}}
{{paramName}}.forEach((element) => {
localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', element as any);
})
{{/isCollectionFormatMulti}}
{{^isCollectionFormatMulti}}
localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}}));
{{/isCollectionFormatMulti}}
}{{/isArray}}
{{^isArray}}
if ({{paramName}} !== undefined) { {{^multipartFormData}}
localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}}
localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}}
localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isEnum}}{{^isEnum}}
localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isEnum}}{{/isPrimitiveType}}{{/multipartFormData}}
}{{/isArray}}
{{/formParams}}{{/vendorExtensions}}
{{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}}
localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded';{{/multipartFormData}}{{#multipartFormData}}
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';{{/multipartFormData}}
{{/hasFormParams}}{{/vendorExtensions}}
{{#bodyParam}}
{{^consumes}}
localVarHeaderParameter['Content-Type'] = 'application/json';
{{/consumes}}
{{#consumes.0}}
localVarHeaderParameter['Content-Type'] = '{{{mediaType}}}';
{{/consumes.0}}
{{/bodyParam}}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions,{{#hasFormParams}}{{#multipartFormData}} ...(localVarFormParams as any).getHeaders?.(),{{/multipartFormData}}{{/hasFormParams}} ...options.headers};
{{#hasFormParams}}
localVarRequestOptions.data = localVarFormParams{{#vendorExtensions}}{{^multipartFormData}}.toString(){{/multipartFormData}}{{/vendorExtensions}};
{{/hasFormParams}}
{{#bodyParam}}
localVarRequestOptions.data = serializeDataIfNeeded({{paramName}}, localVarRequestOptions, configuration)
{{/bodyParam}}
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
{{/operation}}
}
};
/**
* {{classname}} - functional programming interface{{#description}}
* {{{.}}}{{/description}}
* @export
*/
export const {{classname}}Fp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = {{classname}}AxiosParamCreator(configuration)
return {
{{#operation}}
/**
* {{&notes}}
{{#summary}}
* @summary {{&summary}}
{{/summary}}
{{#allParams}}
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
{{/allParams}}
* @param {*} [options] Override http request option.{{#isDeprecated}}
* @deprecated{{/isDeprecated}}
* @throws {RequiredError}
*/
async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
{{/operation}}
}
};
/**
* {{classname}} - factory interface{{#description}}
* {{&description}}{{/description}}
* @export
*/
export const {{classname}}Factory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = {{classname}}Fp(configuration)
return {
{{#operation}}
/**
* {{&notes}}
{{#summary}}
* @summary {{&summary}}
{{/summary}}
{{#allParams}}
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
{{/allParams}}
* @param {*} [options] Override http request option.{{#isDeprecated}}
* @deprecated{{/isDeprecated}}
* @throws {RequiredError}
*/
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: any): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}> {
return localVarFp.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(axios, basePath));
},
{{/operation}}
};
};
{{#withInterfaces}}
/**
* {{classname}} - interface{{#description}}
* {{&description}}{{/description}}
* @export
* @interface {{classname}}
*/
export interface {{classname}}Interface {
{{#operation}}
/**
* {{&notes}}
{{#summary}}
* @summary {{&summary}}
{{/summary}}
{{#allParams}}
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
{{/allParams}}
* @param {*} [options] Override http request option.{{#isDeprecated}}
* @deprecated{{/isDeprecated}}
* @throws {RequiredError}
* @memberof {{classname}}Interface
*/
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>;
{{/operation}}
}
{{/withInterfaces}}
{{#useSingleRequestParameter}}
{{#operation}}
{{#allParams.0}}
/**
* Request parameters for {{nickname}} operation in {{classname}}.
* @export
* @interface {{classname}}{{operationIdCamelCase}}Request
*/
export interface {{classname}}{{operationIdCamelCase}}Request {
{{#allParams}}
/**
* {{description}}
* @type {{=<% %>=}}{<%&dataType%>}<%={{ }}=%>
* @memberof {{classname}}{{operationIdCamelCase}}
*/
readonly {{paramName}}{{^required}}?{{/required}}: {{{dataType}}}
{{^-last}}
{{/-last}}
{{/allParams}}
}
{{/allParams.0}}
{{/operation}}
{{/useSingleRequestParameter}}
/**
* {{classname}} - object-oriented interface{{#description}}
* {{{.}}}{{/description}}
* @export
* @class {{classname}}
* @extends {BaseAPI}
*/
{{#withInterfaces}}
export class {{classname}} extends BaseAPI implements {{classname}}Interface {
{{/withInterfaces}}
{{^withInterfaces}}
export class {{classname}} extends BaseAPI {
{{/withInterfaces}}
{{#operation}}
/**
* {{&notes}}
{{#summary}}
* @summary {{&summary}}
{{/summary}}
{{#useSingleRequestParameter}}
{{#allParams.0}}
* @param {{=<% %>=}}{<%& classname %><%& operationIdCamelCase %>Request}<%={{ }}=%> requestParameters Request parameters.
{{/allParams.0}}
{{/useSingleRequestParameter}}
{{^useSingleRequestParameter}}
{{#allParams}}
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
{{/allParams}}
{{/useSingleRequestParameter}}
* @param {*} [options] Override http request option.{{#isDeprecated}}
* @deprecated{{/isDeprecated}}
* @throws {RequiredError}
* @memberof {{classname}}
*/
{{#useSingleRequestParameter}}
public {{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options?: AxiosRequestConfig) {
return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams.0}}{{#allParams}}requestParameters.{{paramName}}, {{/allParams}}{{/allParams.0}}options).then((request) => request(this.axios, this.basePath));
}
{{/useSingleRequestParameter}}
{{^useSingleRequestParameter}}
public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig) {
return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(this.axios, this.basePath));
}
{{/useSingleRequestParameter}}
{{^-last}}
{{/-last}}
{{/operation}}
}
{{/operations}}

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